diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 270de2e91acb..0f332ce81c46 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -346,6 +346,68 @@ typedef struct opendal_result_operator_writer { struct opendal_error *error; } opendal_result_operator_writer; +/** + * \brief Options for read operations used by C side. + * + * \note For detail description of each field, please refer to [`core::ReadOptions`] + */ +typedef struct opendal_operator_options_read { + /** + * Set `range` for this operation. + */ + const uint64_t *range; + /** + * Set `version` for this operation. + */ + const char *version; + /** + * Set `if_match` for this operation. + */ + const char *if_match; + /** + * Set `if_none_match` for this operation. + */ + const char *if_none_match; + /** + * Set `if_modified_since` for this operation. + * + * \note The value should be in RFC 3339 format. + */ + const char *if_modified_since; + /** + * Set `if_unmodified_since` for this operation. + * + * \note The value should be in RFC 3339 format. + */ + const char *if_unmodified_since; + /** + * Set `concurrent` for the operation. + * + * \note for we do not provide default value in C, so it must be Option in C side. + */ + const uintptr_t *concurrent; + /** + * Set `chunk` for the operation. + */ + const uintptr_t *chunk; + /** + * Controls the optimization strategy for range reads in [`Reader::fetch`]. + */ + const uintptr_t *gap; + /** + * Specify the content-type header that should be sent back by the operation. + */ + const char *override_content_type; + /** + * Specify the `cache-control` header that should be sent back by the operation. + */ + const char *override_cache_control; + /** + * Specify the `content-disposition` header that should be sent back by the operation. + */ + const char *override_content_disposition; +} opendal_operator_options_read; + /** * \brief The result type returned by opendal_operator_is_exist(). * @@ -963,6 +1025,56 @@ struct opendal_result_operator_reader opendal_operator_reader(const struct opend struct opendal_result_operator_writer opendal_operator_writer(const struct opendal_operator *op, const char *path); +/** + * \brief Blocking read the data from `path` with additional options. + * + * Read the data out from `path` blocking by operator with additional options. + * + * @param op The opendal_operator created previously + * @param path The path you want to read the data out + * @param opts The options for read operations + * @see opendal_operator + * @see opendal_result_read + * @see opendal_operator_options_read + * @see opendal_error + * @return Returns opendal_result_read, the `data` field is a pointer to a newly allocated + * opendal_bytes, the `error` field contains the error. If the `error` is not NULL, then + * the operation failed and the `data` field is a nullptr. + * + * \note If the read operation succeeds, the returned opendal_bytes is newly allocated on heap. + * After your usage of that, please call opendal_bytes_free() to free the space. + * + * # Example + * + * Following is an example + * ```C + * // ... you have either write "Hello, World!" to path "/testpath" and created an operator named op + * + * opendal_operator_options_read opts = {}; + * uint64_t range[2] = {0, 14}; + * opts.range = range; + * opendal_result_read r = opendal_operator_read_options(op, "testpath", opts); + * assert(r.error == NULL); + * + * opendal_bytes bytes = r.data; + * assert(bytes.len == 13); + * opendal_bytes_free(&bytes); + * ``` + * + * # Safety + * + * 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. + * + * # Panic + * + * * If the `path` points to NULL, this function panics, i.e. exits with information + */ +struct opendal_result_read opendal_operator_read_options(const struct opendal_operator *op, + const char *path, + const struct opendal_operator_options_read *opts); + /** * \brief Blocking delete the object in `path`. * diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 1156428cb85c..4942adcfb0c9 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -72,3 +72,6 @@ pub use reader::opendal_reader; mod writer; pub use writer::opendal_writer; + +mod options; +pub use options::opendal_operator_options_read; diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs index 428ce4439293..5be51e10dc01 100644 --- a/bindings/c/src/operator.rs +++ b/bindings/c/src/operator.rs @@ -415,6 +415,82 @@ pub unsafe extern "C" fn opendal_operator_writer( } } +/// \brief Blocking read the data from `path` with additional options. +/// +/// Read the data out from `path` blocking by operator with additional options. +/// +/// @param op The opendal_operator created previously +/// @param path The path you want to read the data out +/// @param opts The options for read operations +/// @see opendal_operator +/// @see opendal_result_read +/// @see opendal_operator_options_read +/// @see opendal_error +/// @return Returns opendal_result_read, the `data` field is a pointer to a newly allocated +/// opendal_bytes, the `error` field contains the error. If the `error` is not NULL, then +/// the operation failed and the `data` field is a nullptr. +/// +/// \note If the read operation succeeds, the returned opendal_bytes is newly allocated on heap. +/// After your usage of that, please call opendal_bytes_free() to free the space. +/// +/// # Example +/// +/// Following is an example +/// ```C +/// // ... you have either write "Hello, World!" to path "/testpath" and created an operator named op +/// +/// opendal_operator_options_read opts = {}; +/// uint64_t range[2] = {0, 14}; +/// opts.range = range; +/// opendal_result_read r = opendal_operator_read_options(op, "testpath", opts); +/// assert(r.error == NULL); +/// +/// opendal_bytes bytes = r.data; +/// assert(bytes.len == 13); +/// opendal_bytes_free(&bytes); +/// ``` +/// +/// # Safety +/// +/// 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. +/// +/// # Panic +/// +/// * If the `path` points to NULL, this function panics, i.e. exits with information +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_read_options( + op: &opendal_operator, + path: *const c_char, + opts: *const opendal_operator_options_read, +) -> opendal_result_read { + assert!(!path.is_null()); + let path = std::ffi::CStr::from_ptr(path) + .to_str() + .expect("malformed path"); + let opts = match options::parse_read_options(opts) { + Ok(opts) => opts, + Err(e) => { + return opendal_result_read { + data: opendal_bytes::empty(), + error: opendal_error::new(e), + }; + } + }; + + match op.deref().read_options(path, opts) { + Ok(b) => opendal_result_read { + data: opendal_bytes::new(b), + error: std::ptr::null_mut(), + }, + Err(e) => opendal_result_read { + data: opendal_bytes::empty(), + error: opendal_error::new(e), + }, + } +} + /// \brief Blocking delete the object in `path`. /// /// Delete the object in `path` blocking by `op_ptr`. diff --git a/bindings/c/src/options.rs b/bindings/c/src/options.rs new file mode 100644 index 000000000000..737343a3bd53 --- /dev/null +++ b/bindings/c/src/options.rs @@ -0,0 +1,158 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use ::opendal as core; +use opendal::{ + options::ReadOptions, + raw::{BytesRange, Timestamp}, + Error, +}; +use std::{os::raw::c_char, str::FromStr}; + +/// \brief Options for read operations used by C side. +/// +/// \note For detail description of each field, please refer to [`core::ReadOptions`] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct opendal_operator_options_read { + /// Set `range` for this operation. + pub range: *const u64, + /// Set `version` for this operation. + pub version: *const c_char, + /// Set `if_match` for this operation. + pub if_match: *const c_char, + /// Set `if_none_match` for this operation. + pub if_none_match: *const c_char, + /// Set `if_modified_since` for this operation. + /// + /// \note The value should be in RFC 3339 format. + pub if_modified_since: *const c_char, + /// Set `if_unmodified_since` for this operation. + /// + /// \note The value should be in RFC 3339 format. + pub if_unmodified_since: *const c_char, + /// Set `concurrent` for the operation. + /// + /// \note for we do not provide default value in C, so it must be Option in C side. + pub concurrent: *const usize, + /// Set `chunk` for the operation. + pub chunk: *const usize, + /// Controls the optimization strategy for range reads in [`Reader::fetch`]. + pub gap: *const usize, + /// Specify the content-type header that should be sent back by the operation. + pub override_content_type: *const c_char, + /// Specify the `cache-control` header that should be sent back by the operation. + pub override_cache_control: *const c_char, + /// Specify the `content-disposition` header that should be sent back by the operation. + pub override_content_disposition: *const c_char, +} + +impl opendal_operator_options_read {} + +pub fn parse_read_options( + options: *const opendal_operator_options_read, +) -> Result { + // if original opts is blank, we will use the default options + let mut opts = ReadOptions::default(); + + unsafe { + let options = *options; + if !options.range.is_null() { + // TODO: + // Do we need to make sure it has no more than 2 usize? + let range = std::slice::from_raw_parts(options.range, 2); + opts.range = BytesRange::new(range[0], Some(range[1])); + } + if !options.version.is_null() { + opts.version = Some( + std::ffi::CStr::from_ptr(options.version) + .to_str() + .expect("malformed version") + .to_string(), + ); + } + if !options.if_match.is_null() { + opts.if_match = Some( + std::ffi::CStr::from_ptr(options.if_match) + .to_str() + .expect("malformed if_match") + .to_string(), + ); + } + if !options.if_none_match.is_null() { + opts.if_none_match = Some( + std::ffi::CStr::from_ptr(options.if_none_match) + .to_str() + .expect("malformed if_none_match") + .to_string(), + ); + } + if !options.if_modified_since.is_null() { + let ts_str = std::ffi::CStr::from_ptr(options.if_modified_since) + .to_str() + .expect("malformed if_modified_since") + .to_string(); + + let ts = Timestamp::from_str(&ts_str)?; + opts.if_modified_since = Some(ts); + } + if !options.if_unmodified_since.is_null() { + let ts_str = std::ffi::CStr::from_ptr(options.if_unmodified_since) + .to_str() + .expect("malformed if_unmodified_since") + .to_string(); + + let ts = Timestamp::from_str(&ts_str)?; + opts.if_unmodified_since = Some(ts); + } + if !options.concurrent.is_null() { + opts.concurrent = *options.concurrent; + } + if !options.chunk.is_null() { + opts.chunk = Some(*options.chunk); + } + if !options.gap.is_null() { + opts.gap = Some(*options.gap); + } + if !options.override_content_type.is_null() { + opts.override_content_type = Some( + std::ffi::CStr::from_ptr(options.override_content_type) + .to_str() + .expect("malformed override_content_type") + .to_string(), + ); + } + if !options.override_cache_control.is_null() { + opts.override_cache_control = Some( + std::ffi::CStr::from_ptr(options.override_cache_control) + .to_str() + .expect("malformed override_cache_control") + .to_string(), + ); + } + if !options.override_content_disposition.is_null() { + opts.override_content_disposition = Some( + std::ffi::CStr::from_ptr(options.override_content_disposition) + .to_str() + .expect("malformed override_content_disposition") + .to_string(), + ); + } + } + + Ok(opts) +} diff --git a/bindings/c/tests/Makefile b/bindings/c/tests/Makefile index 11344eba8ddc..d592b67affe8 100644 --- a/bindings/c/tests/Makefile +++ b/bindings/c/tests/Makefile @@ -26,7 +26,7 @@ endif # Source files FRAMEWORK_SOURCES = test_framework.cpp -SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp +SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp test_suites_options.cpp RUNNER_SOURCES = test_runner.cpp ALL_SOURCES = $(FRAMEWORK_SOURCES) $(SUITE_SOURCES) $(RUNNER_SOURCES) diff --git a/bindings/c/tests/README.md b/bindings/c/tests/README.md index 516f6ef35dff..3759a6b62899 100644 --- a/bindings/c/tests/README.md +++ b/bindings/c/tests/README.md @@ -46,6 +46,7 @@ The test framework provides: - **`test_suites_basic.cpp`**: Basic CRUD operations (check, write, read, exists, stat, delete, create_dir) - **`test_suites_list.cpp`**: Directory listing and traversal operations - **`test_suites_reader_writer.cpp`**: Streaming read/write operations with seek functionality +- **`test_suites_options.cpp`**: options read/write operations with seek functionality - **`test_runner.cpp`**: Main executable with command-line interface ### Test Structure @@ -191,6 +192,12 @@ Tests streaming I/O operations: - **writer_large_data**: Write large amounts of data - **reader_partial_read**: Read files in chunks +### Options Operations +Tests options read/write operations: +- **read_options_default**: Read options with default values +- **read_options_range**: Read options with range configured +- **read_options_timestamp**: Read options with if_modified_since configured + ## Assertion Macros The framework provides comprehensive assertion macros: diff --git a/bindings/c/tests/test_runner.cpp b/bindings/c/tests/test_runner.cpp index 62e013549f5a..c939f3ac5631 100644 --- a/bindings/c/tests/test_runner.cpp +++ b/bindings/c/tests/test_runner.cpp @@ -25,12 +25,14 @@ extern opendal_test_suite basic_suite; extern opendal_test_suite list_suite; extern opendal_test_suite reader_writer_suite; +extern opendal_test_suite options_suite; // List of all test suites static opendal_test_suite* all_suites[] = { &basic_suite, &list_suite, &reader_writer_suite, + &options_suite, }; static const size_t num_suites = sizeof(all_suites) / sizeof(all_suites[0]); diff --git a/bindings/c/tests/test_suites_options.cpp b/bindings/c/tests/test_suites_options.cpp new file mode 100644 index 000000000000..9d5e04e5c81d --- /dev/null +++ b/bindings/c/tests/test_suites_options.cpp @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "test_framework.h" + +// Test: Default options read operation +void test_read_options_default(opendal_test_context* ctx) +{ + const char* path = "test_read_options.txt"; + const char* content = "Hello, OpenDAL Read Options!"; + size_t content_len = strlen(content); + + // Write test data first + opendal_bytes data = { + .data = (uint8_t*)content, + .len = content_len, + .capacity = content_len + }; + + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); + OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); + + // Read datat back with default options + opendal_operator_options_read opts = {}; + opendal_result_read result = opendal_operator_read_options(ctx->config->operator_instance, path, &opts); + OPENDAL_ASSERT_NO_ERROR(result.error, "Options Read operation should succeed"); + OPENDAL_ASSERT_EQ(content_len, result.data.len, + "Read data length should match written data"); + + // Verify content + OPENDAL_ASSERT(memcmp(content, result.data.data, result.data.len) == 0, + "Read content should match written content"); + + // Cleanup + opendal_bytes_free(&result.data); + opendal_operator_delete(ctx->config->operator_instance, path); +} + +// Test: range options read operation +void test_read_options_range(opendal_test_context* ctx) +{ + const char* path = "test_read_options.txt"; + const char* content = "Hello, OpenDAL Read Options!"; + size_t content_len = strlen(content); + + // Write test data first + opendal_bytes data = { + .data = (uint8_t*)content, + .len = content_len, + .capacity = content_len + }; + + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); + OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); + + // Read datat back with default options + opendal_operator_options_read opts = {}; + uint64_t range[2] = { 0, 14 }; + opts.range = range; + opendal_result_read result = opendal_operator_read_options(ctx->config->operator_instance, path, &opts); + OPENDAL_ASSERT_NO_ERROR(result.error, "Options Read operation should succeed"); + OPENDAL_ASSERT_EQ(14, result.data.len, + "Read data length should match written data"); + + // Verify content + OPENDAL_ASSERT(memcmp(content, result.data.data, result.data.len) == 0, + "Read content should match written content"); + + // Cleanup + opendal_bytes_free(&result.data); + opendal_operator_delete(ctx->config->operator_instance, path); +} + +// Define the options read/write test suite +opendal_test_case options_tests[] = { + { "read_options_default", test_read_options_default, make_capability_read_write() }, + { "read_options_range", test_read_options_range, make_capability_read_write() }, +}; + +opendal_test_suite options_suite = { + "Options Operations", // name + options_tests, // tests + sizeof(options_tests) / sizeof(options_tests[0]) // test_count +};