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

kontinuation pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new d748e4c0 feat(sedona-gdal): add foundational wrapper utilities (#696)
d748e4c0 is described below

commit d748e4c092692697dceb4ac26bd168feb704b164
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Fri Mar 13 14:08:30 2026 +0800

    feat(sedona-gdal): add foundational wrapper utilities (#696)
    
    ## Summary
    - add foundational safe-wrapper utilities on top of the GDAL FFI crate
    - introduce GDAL option list helpers, raster type abstractions, and 
expanded shared error handling
---
 c/sedona-gdal/src/{errors.rs => config.rs} |  38 +-
 c/sedona-gdal/src/cpl.rs                   | 690 +++++++++++++++++++++++++++++
 c/sedona-gdal/src/errors.rs                |  10 +
 c/sedona-gdal/src/gdal_api.rs              |   2 +
 c/sedona-gdal/src/lib.rs                   |   5 +
 c/sedona-gdal/src/{lib.rs => raster.rs}    |  11 +-
 c/sedona-gdal/src/raster/types.rs          | 212 +++++++++
 7 files changed, 939 insertions(+), 29 deletions(-)

diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/config.rs
similarity index 58%
copy from c/sedona-gdal/src/errors.rs
copy to c/sedona-gdal/src/config.rs
index 166e0423..2735d6b5 100644
--- a/c/sedona-gdal/src/errors.rs
+++ b/c/sedona-gdal/src/config.rs
@@ -16,27 +16,27 @@
 // under the License.
 
 //! Ported (and contains copied code) from georust/gdal:
-//! <https://github.com/georust/gdal/blob/v0.19.0/src/errors.rs>.
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/config.rs>.
 //! Original code is licensed under MIT.
+//!
+//! GDAL configuration option wrappers.
 
-use thiserror::Error;
+use std::ffi::CString;
 
-/// Error type for the sedona-gdal crate initialization and library loading.
-#[derive(Error, Debug)]
-pub enum GdalInitLibraryError {
-    #[error("{0}")]
-    Invalid(String),
-    #[error("{0}")]
-    LibraryError(String),
-}
+use crate::errors::Result;
+use crate::gdal_api::{call_gdal_api, GdalApi};
 
-/// Error type compatible with the georust/gdal error variants used in this 
codebase.
-#[derive(Clone, Debug, Error)]
-pub enum GdalError {
-    #[error("CPL error class: '{class:?}', error number: '{number}', error 
msg: '{msg}'")]
-    CplError {
-        class: u32,
-        number: i32,
-        msg: String,
-    },
+/// Set a GDAL library configuration option with **thread-local** scope.
+pub fn set_thread_local_config_option(api: &'static GdalApi, key: &str, value: 
&str) -> Result<()> {
+    let c_key = CString::new(key)?;
+    let c_val = CString::new(value)?;
+    unsafe {
+        call_gdal_api!(
+            api,
+            CPLSetThreadLocalConfigOption,
+            c_key.as_ptr(),
+            c_val.as_ptr()
+        );
+    }
+    Ok(())
 }
diff --git a/c/sedona-gdal/src/cpl.rs b/c/sedona-gdal/src/cpl.rs
new file mode 100644
index 00000000..b09e9f77
--- /dev/null
+++ b/c/sedona-gdal/src/cpl.rs
@@ -0,0 +1,690 @@
+// 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.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/cpl.rs>.
+//! Original code is licensed under MIT.
+//!
+//! GDAL Common Portability Library Functions.
+//!
+//! Provides [`CslStringList`], a pure-Rust implementation of GDAL's 
null-terminated
+//! string list (`char **papszStrList`), compatible with the georust/gdal API 
surface.
+
+use std::ffi::{c_char, CString};
+use std::fmt::{Debug, Display, Formatter};
+use std::ptr;
+
+use crate::errors::{GdalError, Result};
+
+/// A null-terminated array of null-terminated C strings (`char 
**papszStrList`).
+///
+/// This data structure is used throughout GDAL to pass `KEY=VALUE`-formatted 
options
+/// to various functions.
+///
+/// This is a pure Rust implementation that mirrors the API of georust/gdal's
+/// `CslStringList`. Memory is managed entirely in Rust — no GDAL `CSL*` 
functions
+/// are called for list management. This should be fine as long as GDAL does 
not
+/// take ownership of the string lists and free them using `CSLDestroy`.
+///
+/// # Example
+///
+/// There are a number of ways to populate a [`CslStringList`]:
+///
+/// ```rust
+/// use sedona_gdal::cpl::{CslStringList, CslStringListEntry};
+///
+/// let mut sl1 = CslStringList::new();
+/// sl1.set_name_value("NUM_THREADS", "ALL_CPUS").unwrap();
+/// sl1.set_name_value("COMPRESS", "LZW").unwrap();
+/// sl1.add_string("MAGIC_FLAG").unwrap();
+///
+/// let sl2 = CslStringList::try_from_iter(["NUM_THREADS=ALL_CPUS", 
"COMPRESS=LZW", "MAGIC_FLAG"]).unwrap();
+///
+/// assert_eq!(sl1.to_string(), sl2.to_string());
+/// ```
+pub struct CslStringList {
+    /// Owned strings.
+    strings: Vec<CString>,
+    /// Null-terminated pointer array into `strings`, rebuilt on every 
mutation.
+    /// Invariant: `ptrs.len() == strings.len() + 1` and `ptrs.last() == 
Some(&null_mut())`.
+    ptrs: Vec<*mut c_char>,
+}
+
+// Safety: CslStringList is Send + Sync because:
+// - `strings` (Vec<CString>) is Send + Sync.
+// - `ptrs` contains pointers derived from `strings` (stable heap-allocated 
CString data).
+//   They are only used for read-only FFI calls.
+unsafe impl Send for CslStringList {}
+unsafe impl Sync for CslStringList {}
+
+impl CslStringList {
+    /// Creates an empty GDAL string list.
+    pub fn new() -> Self {
+        Self::with_capacity(0)
+    }
+
+    /// Create an empty GDAL string list with given capacity.
+    pub fn with_capacity(capacity: usize) -> Self {
+        let mut ptrs = Vec::with_capacity(capacity + 1);
+        ptrs.push(ptr::null_mut());
+        Self {
+            strings: Vec::with_capacity(capacity),
+            ptrs,
+        }
+    }
+
+    /// Rebuilds the null-terminated pointer array from `self.strings`.
+    ///
+    /// Must be called after every mutation to `self.strings`.
+    /// This is O(n) but n is always small (option lists are typically < 20 
entries).
+    ///
+    /// Safety argument: `CString` stores its data on the heap. Moving a 
`CString`
+    /// (as happens during `Vec` reallocation) does not invalidate the heap 
pointer
+    /// returned by `CString::as_ptr()`. Therefore pointers stored in 
`self.ptrs`
+    /// remain valid as long as the corresponding `CString` in `self.strings` 
is alive.
+    fn rebuild_ptrs(&mut self) {
+        self.ptrs.clear();
+        for s in &self.strings {
+            self.ptrs.push(s.as_ptr() as *mut c_char);
+        }
+        self.ptrs.push(ptr::null_mut());
+    }
+
+    /// Check that the given `name` is a valid [`CslStringList`] key.
+    ///
+    /// Per [GDAL 
documentation](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc),
+    /// a key cannot have non-alphanumeric characters in it (underscores are 
allowed).
+    ///
+    /// Returns `Err(GdalError::BadArgument)` on invalid name, `Ok(())` 
otherwise.
+    fn check_valid_name(name: &str) -> Result<()> {
+        if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
+            Err(GdalError::BadArgument(format!(
+                "Invalid characters in name: '{name}'"
+            )))
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Check that the given `value` is a valid [`CslStringList`] value.
+    ///
+    /// Per [GDAL 
documentation](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc),
+    /// a value cannot have newline characters in it.
+    ///
+    /// Returns `Err(GdalError::BadArgument)` on invalid value, `Ok(())` 
otherwise.
+    fn check_valid_value(value: &str) -> Result<()> {
+        if value.contains(['\n', '\r']) {
+            Err(GdalError::BadArgument(format!(
+                "Invalid characters in value: '{value}'"
+            )))
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Assigns `value` to the key `name` without checking for pre-existing 
assignments.
+    ///
+    /// Returns `Ok(())` on success, or `Err(GdalError::BadArgument)`
+    /// if `name` has non-alphanumeric characters or `value` has newline 
characters.
+    ///
+    /// See: 
[`CSLAddNameValue`](https://gdal.org/api/cpl.html#_CPPv415CSLAddNameValuePPcPKcPKc)
+    /// for details.
+    pub fn add_name_value(&mut self, name: &str, value: &str) -> Result<()> {
+        Self::check_valid_name(name)?;
+        Self::check_valid_value(value)?;
+        let entry = CString::new(format!("{name}={value}"))?;
+        self.strings.push(entry);
+        self.rebuild_ptrs();
+        Ok(())
+    }
+
+    /// Assigns `value` to the key `name`, overwriting any existing assignment 
to `name`.
+    ///
+    /// Name lookup is case-insensitive, matching GDAL's `CSLSetNameValue` 
behavior.
+    ///
+    /// Returns `Ok(())` on success, or `Err(GdalError::BadArgument)`
+    /// if `name` has non-alphanumeric characters or `value` has newline 
characters.
+    ///
+    /// See: 
[`CSLSetNameValue`](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc)
+    /// for details.
+    pub fn set_name_value(&mut self, name: &str, value: &str) -> Result<()> {
+        Self::check_valid_name(name)?;
+        Self::check_valid_value(value)?;
+        let existing = self.strings.iter().position(|s| {
+            s.to_str().is_ok_and(|v| {
+                v.split_once('=')
+                    .is_some_and(|(k, _)| k.eq_ignore_ascii_case(name))
+            })
+        });
+        let new_entry = CString::new(format!("{name}={value}"))?;
+        if let Some(idx) = existing {
+            self.strings[idx] = new_entry;
+        } else {
+            self.strings.push(new_entry);
+        }
+        self.rebuild_ptrs();
+        Ok(())
+    }
+
+    /// Adds a copy of the string slice `value` to the list.
+    ///
+    /// Returns `Ok(())` on success, `Err(GdalError::FfiNulError)` if `value` 
cannot be
+    /// converted to a C string (e.g. `value` contains a `0` byte).
+    ///
+    /// See: 
[`CSLAddString`](https://gdal.org/api/cpl.html#_CPPv412CSLAddStringPPcPKc)
+    pub fn add_string(&mut self, value: &str) -> Result<()> {
+        let v = CString::new(value)?;
+        self.strings.push(v);
+        self.rebuild_ptrs();
+        Ok(())
+    }
+
+    /// Adds the contents of a [`CslStringListEntry`] to `self`.
+    ///
+    /// Returns `Err(GdalError::BadArgument)` if entry doesn't meet entry 
restrictions as
+    /// described by [`CslStringListEntry`].
+    pub fn add_entry(&mut self, entry: &CslStringListEntry) -> Result<()> {
+        match entry {
+            CslStringListEntry::Flag(f) => self.add_string(f),
+            CslStringListEntry::Pair { name, value } => 
self.add_name_value(name, value),
+        }
+    }
+
+    /// Looks up the value corresponding to `name` (case-insensitive).
+    ///
+    /// See 
[`CSLFetchNameValue`](https://gdal.org/doxygen/cpl__string_8h.html#a4f23675f8b6f015ed23d9928048361a1)
+    /// for details.
+    pub fn fetch_name_value(&self, name: &str) -> Option<String> {
+        for s in &self.strings {
+            if let Ok(v) = s.to_str() {
+                if let Some((k, val)) = v.split_once('=') {
+                    if k.eq_ignore_ascii_case(name) {
+                        return Some(val.to_string());
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    /// Perform a case **insensitive** search for the given string.
+    ///
+    /// Returns `Some(usize)` of value index position, or `None` if not found.
+    ///
+    /// See: 
[`CSLFindString`](https://gdal.org/api/cpl.html#_CPPv413CSLFindString12CSLConstListPKc)
+    /// for details.
+    pub fn find_string(&self, value: &str) -> Option<usize> {
+        self.strings
+            .iter()
+            .position(|s| s.to_str().is_ok_and(|v| 
v.eq_ignore_ascii_case(value)))
+    }
+
+    /// Perform a case sensitive search for the given string.
+    ///
+    /// Returns `Some(usize)` of value index position, or `None` if not found.
+    pub fn find_string_case_sensitive(&self, value: &str) -> Option<usize> {
+        self.strings.iter().position(|s| s.to_str() == Ok(value))
+    }
+
+    /// Perform a case sensitive partial string search indicated by `fragment`.
+    ///
+    /// Returns `Some(usize)` of value index position, or `None` if not found.
+    ///
+    /// See: 
[`CSLPartialFindString`](https://gdal.org/api/cpl.html#_CPPv420CSLPartialFindString12CSLConstListPKc)
+    /// for details.
+    pub fn partial_find_string(&self, fragment: &str) -> Option<usize> {
+        self.strings
+            .iter()
+            .position(|s| s.to_str().is_ok_and(|v| v.contains(fragment)))
+    }
+
+    /// Fetch the [`CslStringListEntry`] for the entry at the given index.
+    ///
+    /// Returns `None` if index is out of bounds, `Some(entry)` otherwise.
+    pub fn get_field(&self, index: usize) -> Option<CslStringListEntry> {
+        self.strings
+            .get(index)
+            .and_then(|s| s.to_str().ok())
+            .map(CslStringListEntry::from)
+    }
+
+    /// Determine the number of entries in the list.
+    ///
+    /// See: 
[`CSLCount`](https://gdal.org/api/cpl.html#_CPPv48CSLCount12CSLConstList) for 
details.
+    pub fn len(&self) -> usize {
+        self.strings.len()
+    }
+
+    /// Determine if the list has any values.
+    pub fn is_empty(&self) -> bool {
+        self.strings.is_empty()
+    }
+
+    /// Get an iterator over the entries of the list.
+    pub fn iter(&self) -> CslStringListIterator<'_> {
+        CslStringListIterator { list: self, idx: 0 }
+    }
+
+    /// Get the raw null-terminated `char**` pointer for passing to GDAL 
functions.
+    ///
+    /// The returned pointer is valid as long as `self` is alive and not 
mutated.
+    /// An empty list returns a pointer to `[null]`, which is a valid empty 
CSL.
+    pub fn as_ptr(&self) -> *mut *mut c_char {
+        self.ptrs.as_ptr() as *mut *mut c_char
+    }
+
+    /// Truncate the list to at most `len` entries.
+    pub fn truncate(&mut self, len: usize) {
+        self.strings.truncate(len);
+        self.rebuild_ptrs();
+    }
+
+    /// Extend the list from an iterator, rolling back to the original size on 
error.
+    pub fn try_extend<T, I>(&mut self, iter: I) -> Result<()>
+    where
+        I: IntoIterator<Item = T>,
+        T: Into<CslStringListEntry>,
+    {
+        let original_len = self.len();
+        for item in iter {
+            let entry = item.into();
+            if let Err(err) = self.add_entry(&entry) {
+                self.truncate(original_len);
+                return Err(err);
+            }
+        }
+        Ok(())
+    }
+
+    /// Construct a `CslStringList` from a fallible iterator of entries.
+    pub fn try_from_iter<T, I>(iter: I) -> Result<Self>
+    where
+        I: IntoIterator<Item = T>,
+        T: Into<CslStringListEntry>,
+    {
+        let mut list = Self::new();
+        list.try_extend(iter)?;
+        Ok(list)
+    }
+}
+
+impl Default for CslStringList {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Clone for CslStringList {
+    fn clone(&self) -> Self {
+        let strings = self.strings.clone();
+        let mut result = Self {
+            strings,
+            ptrs: Vec::new(),
+        };
+        result.rebuild_ptrs();
+        result
+    }
+}
+
+impl Debug for CslStringList {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let mut b = f.debug_tuple("CslStringList");
+        for e in self.iter() {
+            b.field(&e.to_string());
+        }
+        b.finish()
+    }
+}
+
+impl Display for CslStringList {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        for e in self.iter() {
+            f.write_fmt(format_args!("{e}\n"))?;
+        }
+        Ok(())
+    }
+}
+
+impl<'a> IntoIterator for &'a CslStringList {
+    type Item = CslStringListEntry;
+    type IntoIter = CslStringListIterator<'a>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
+/// Represents an entry in a [`CslStringList`].
+///
+/// An entry is either a single token ([`Flag`](Self::Flag)), or a `name=value`
+/// assignment ([`Pair`](Self::Pair)).
+///
+/// Note: When constructed directly, assumes string values do not contain 
newline characters
+/// nor the null `\0` character. If these conditions are violated, the 
provided values will
+/// be ignored.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum CslStringListEntry {
+    /// A single token entry.
+    Flag(String),
+    /// A `name=value` pair entry.
+    Pair { name: String, value: String },
+}
+
+impl CslStringListEntry {
+    /// Create a new [`Self::Flag`] entry.
+    pub fn new_flag(flag: &str) -> Self {
+        CslStringListEntry::Flag(flag.to_owned())
+    }
+
+    /// Create a new [`Self::Pair`] entry.
+    pub fn new_pair(name: &str, value: &str) -> Self {
+        CslStringListEntry::Pair {
+            name: name.to_owned(),
+            value: value.to_owned(),
+        }
+    }
+}
+
+impl From<&str> for CslStringListEntry {
+    fn from(value: &str) -> Self {
+        value.to_owned().into()
+    }
+}
+
+impl From<(&str, &str)> for CslStringListEntry {
+    fn from((key, value): (&str, &str)) -> Self {
+        Self::new_pair(key, value)
+    }
+}
+
+impl From<String> for CslStringListEntry {
+    fn from(value: String) -> Self {
+        match value.split_once('=') {
+            Some((name, value)) => Self::new_pair(name, value),
+            None => Self::new_flag(&value),
+        }
+    }
+}
+
+impl From<(String, String)> for CslStringListEntry {
+    fn from((name, value): (String, String)) -> Self {
+        Self::Pair { name, value }
+    }
+}
+
+impl Display for CslStringListEntry {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            CslStringListEntry::Flag(s) => f.write_str(s),
+            CslStringListEntry::Pair { name, value } => 
f.write_fmt(format_args!("{name}={value}")),
+        }
+    }
+}
+
+/// State for iterator over [`CslStringList`] entries.
+pub struct CslStringListIterator<'a> {
+    list: &'a CslStringList,
+    idx: usize,
+}
+
+impl Iterator for CslStringListIterator<'_> {
+    type Item = CslStringListEntry;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let entry = self.list.strings.get(self.idx)?;
+        self.idx += 1;
+        Some(entry.to_string_lossy().into_owned().into())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::errors::Result;
+
+    fn fixture() -> Result<CslStringList> {
+        let mut l = CslStringList::new();
+        l.set_name_value("ONE", "1")?;
+        l.set_name_value("TWO", "2")?;
+        l.set_name_value("THREE", "3")?;
+        l.add_string("SOME_FLAG")?;
+        Ok(l)
+    }
+
+    #[test]
+    fn construct() -> Result<()> {
+        let mut sl1 = CslStringList::new();
+        sl1.set_name_value("NUM_THREADS", "ALL_CPUS").unwrap();
+        sl1.set_name_value("COMPRESS", "LZW").unwrap();
+        sl1.add_string("MAGIC_FLAG").unwrap();
+
+        let sl2 =
+            CslStringList::try_from_iter(["NUM_THREADS=ALL_CPUS", 
"COMPRESS=LZW", "MAGIC_FLAG"])?;
+        let sl3 = CslStringList::try_from_iter([
+            CslStringListEntry::from(("NUM_THREADS", "ALL_CPUS")),
+            CslStringListEntry::from(("COMPRESS", "LZW")),
+            CslStringListEntry::from("MAGIC_FLAG"),
+        ])?;
+
+        assert_eq!(sl1.to_string(), sl2.to_string());
+        assert_eq!(sl2.to_string(), sl3.to_string());
+
+        Ok(())
+    }
+
+    #[test]
+    fn basic_list() -> Result<()> {
+        let l = fixture()?;
+        assert!(matches!(l.fetch_name_value("ONE"), Some(s) if s == *"1"));
+        assert!(matches!(l.fetch_name_value("THREE"), Some(s) if s == *"3"));
+        assert!(l.fetch_name_value("FOO").is_none());
+
+        Ok(())
+    }
+
+    #[test]
+    fn has_length() -> Result<()> {
+        let l = fixture()?;
+        assert_eq!(l.len(), 4);
+
+        Ok(())
+    }
+
+    #[test]
+    fn can_be_empty() -> Result<()> {
+        let l = CslStringList::new();
+        assert!(l.is_empty());
+
+        let l = fixture()?;
+        assert!(!l.is_empty());
+
+        Ok(())
+    }
+
+    #[test]
+    fn has_iterator() -> Result<()> {
+        let f = fixture()?;
+        let mut it = f.iter();
+        assert_eq!(it.next(), Some(("ONE", "1").into()));
+        assert_eq!(it.next(), Some(("TWO", "2").into()));
+        assert_eq!(it.next(), Some(("THREE", "3").into()));
+        assert_eq!(it.next(), Some("SOME_FLAG".into()));
+        assert_eq!(it.next(), None);
+        assert_eq!(it.next(), None);
+        Ok(())
+    }
+
+    #[test]
+    fn invalid_name_value() -> Result<()> {
+        let mut l = fixture()?;
+        assert!(l.set_name_value("l==t", "2").is_err());
+        assert!(l.set_name_value("foo", "2\n4\r5").is_err());
+
+        Ok(())
+    }
+
+    #[test]
+    fn add_vs_set() -> Result<()> {
+        let mut f = CslStringList::new();
+        f.add_name_value("ONE", "1")?;
+        f.add_name_value("ONE", "2")?;
+        let s = f.to_string();
+        assert!(s.contains("ONE") && s.contains('1') && s.contains('2'));
+
+        let mut f = CslStringList::new();
+        f.set_name_value("ONE", "1")?;
+        f.set_name_value("ONE", "2")?;
+        let s = f.to_string();
+        assert!(s.contains("ONE") && !s.contains('1') && s.contains('2'));
+
+        Ok(())
+    }
+
+    #[test]
+    fn try_from_iter_constructs_list() -> Result<()> {
+        let l = CslStringList::try_from_iter(["ONE=1", "TWO=2"])?;
+        assert!(matches!(l.fetch_name_value("ONE"), Some(s) if s == *"1"));
+        assert!(matches!(l.fetch_name_value("TWO"), Some(s) if s == *"2"));
+
+        Ok(())
+    }
+
+    #[test]
+    fn try_from_iter_rejects_invalid_entry() {
+        let result = 
CslStringList::try_from_iter([CslStringListEntry::from(("bad-name", "1"))]);
+        assert!(matches!(result, Err(GdalError::BadArgument(_))));
+    }
+
+    #[test]
+    fn try_extend_is_transactional() -> Result<()> {
+        let mut list = CslStringList::try_from_iter([("ONE", "1"), ("TWO", 
"2")])?;
+        let before = list.clone();
+
+        let result = list.try_extend([
+            CslStringListEntry::from(("THREE", "3")),
+            CslStringListEntry::from(("bad-name", "4")),
+        ]);
+
+        assert!(matches!(result, Err(GdalError::BadArgument(_))));
+        assert_eq!(list.to_string(), before.to_string());
+
+        Ok(())
+    }
+
+    #[test]
+    fn truncate_preserves_ptr_invariant() -> Result<()> {
+        let mut list = CslStringList::try_from_iter(["A", "B", "C"])?;
+        list.truncate(1);
+
+        assert_eq!(list.len(), 1);
+        assert_eq!(list.get_field(0), Some(CslStringListEntry::from("A")));
+        assert_eq!(list.get_field(1), None);
+
+        let ptr = list.as_ptr();
+        unsafe {
+            assert!(!(*ptr).is_null());
+            assert!((*ptr.add(1)).is_null());
+        }
+
+        Ok(())
+    }
+
+    #[test]
+    fn debug_fmt() -> Result<()> {
+        let l = fixture()?;
+        let s = format!("{l:?}");
+        assert!(s.contains("ONE=1"));
+        assert!(s.contains("TWO=2"));
+        assert!(s.contains("THREE=3"));
+        assert!(s.contains("SOME_FLAG"));
+
+        Ok(())
+    }
+
+    #[test]
+    fn can_add_strings() -> Result<()> {
+        let mut l = CslStringList::new();
+        assert!(l.is_empty());
+        l.add_string("-abc")?;
+        l.add_string("-d_ef")?;
+        l.add_string("A")?;
+        l.add_string("B")?;
+        assert_eq!(l.len(), 4);
+
+        Ok(())
+    }
+
+    #[test]
+    fn find_string() -> Result<()> {
+        let f = fixture()?;
+        assert_eq!(f.find_string("NON_FLAG"), None);
+        assert_eq!(f.find_string("SOME_FLAG"), Some(3));
+        assert_eq!(f.find_string("ONE=1"), Some(0));
+        assert_eq!(f.find_string("one=1"), Some(0));
+        assert_eq!(f.find_string("TWO="), None);
+        Ok(())
+    }
+
+    #[test]
+    fn find_string_case_sensitive() -> Result<()> {
+        let f = fixture()?;
+        assert_eq!(f.find_string_case_sensitive("ONE=1"), Some(0));
+        assert_eq!(f.find_string_case_sensitive("one=1"), None);
+        assert_eq!(f.find_string_case_sensitive("SOME_FLAG"), Some(3));
+        Ok(())
+    }
+
+    #[test]
+    fn partial_find_string() -> Result<()> {
+        let f = fixture()?;
+        assert_eq!(f.partial_find_string("ONE=1"), Some(0));
+        assert_eq!(f.partial_find_string("ONE="), Some(0));
+        assert_eq!(f.partial_find_string("=1"), Some(0));
+        assert_eq!(f.partial_find_string("1"), Some(0));
+        assert_eq!(f.partial_find_string("THREE="), Some(2));
+        assert_eq!(f.partial_find_string("THREE"), Some(2));
+        assert_eq!(f.partial_find_string("three"), None);
+        Ok(())
+    }
+
+    #[test]
+    fn as_ptr_is_null_terminated() {
+        let mut l = CslStringList::new();
+        l.add_string("A").unwrap();
+        l.add_string("B").unwrap();
+        let ptr = l.as_ptr();
+        unsafe {
+            // First entry
+            assert!(!(*ptr).is_null());
+            // Second entry
+            assert!(!(*ptr.add(1)).is_null());
+            // Null terminator
+            assert!((*ptr.add(2)).is_null());
+        }
+    }
+
+    #[test]
+    fn clone_is_independent() -> Result<()> {
+        let f = fixture()?;
+        let mut g = f.clone();
+        g.set_name_value("ONE", "999")?;
+        // Original is unchanged.
+        assert_eq!(f.fetch_name_value("ONE"), Some("1".into()));
+        assert_eq!(g.fetch_name_value("ONE"), Some("999".into()));
+        Ok(())
+    }
+}
diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs
index 166e0423..6f2ad1a0 100644
--- a/c/sedona-gdal/src/errors.rs
+++ b/c/sedona-gdal/src/errors.rs
@@ -19,6 +19,8 @@
 //! <https://github.com/georust/gdal/blob/v0.19.0/src/errors.rs>.
 //! Original code is licensed under MIT.
 
+use std::ffi::NulError;
+
 use thiserror::Error;
 
 /// Error type for the sedona-gdal crate initialization and library loading.
@@ -39,4 +41,12 @@ pub enum GdalError {
         number: i32,
         msg: String,
     },
+
+    #[error("Bad argument: {0}")]
+    BadArgument(String),
+
+    #[error("FFI NUL error: {0}")]
+    FfiNulError(#[from] NulError),
 }
+
+pub type Result<T> = std::result::Result<T, GdalError>;
diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs
index d0c07f6f..4b8381a6 100644
--- a/c/sedona-gdal/src/gdal_api.rs
+++ b/c/sedona-gdal/src/gdal_api.rs
@@ -43,6 +43,8 @@ macro_rules! call_gdal_api {
     };
 }
 
+pub(crate) use call_gdal_api;
+
 #[derive(Debug)]
 pub struct GdalApi {
     pub(crate) inner: SedonaGdalApi,
diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs
index e05a3bd0..b64a2275 100644
--- a/c/sedona-gdal/src/lib.rs
+++ b/c/sedona-gdal/src/lib.rs
@@ -25,3 +25,8 @@ pub mod errors;
 // --- Core API ---
 pub mod gdal_api;
 pub mod global;
+
+// --- High-level wrappers ---
+pub mod config;
+pub mod cpl;
+pub mod raster;
diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/raster.rs
similarity index 82%
copy from c/sedona-gdal/src/lib.rs
copy to c/sedona-gdal/src/raster.rs
index e05a3bd0..1ddc9b2e 100644
--- a/c/sedona-gdal/src/lib.rs
+++ b/c/sedona-gdal/src/raster.rs
@@ -15,13 +15,4 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// --- FFI layer ---
-pub(crate) mod dyn_load;
-pub mod gdal_dyn_bindgen;
-
-// --- Error types ---
-pub mod errors;
-
-// --- Core API ---
-pub mod gdal_api;
-pub mod global;
+pub mod types;
diff --git a/c/sedona-gdal/src/raster/types.rs 
b/c/sedona-gdal/src/raster/types.rs
new file mode 100644
index 00000000..49ba23b8
--- /dev/null
+++ b/c/sedona-gdal/src/raster/types.rs
@@ -0,0 +1,212 @@
+// 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.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/raster/types.rs>.
+//! Original code is licensed under MIT.
+
+use crate::gdal_dyn_bindgen::{
+    self, GDALDataType, GDALOpenFlags, GDALRIOResampleAlg, GDAL_OF_READONLY, 
GDAL_OF_VERBOSE_ERROR,
+};
+
+/// A Rust-friendly enum mirroring the georust/gdal `GdalDataType` names.
+///
+/// This maps 1-to-1 with [`GDALDataType`] but uses Rust-idiomatic names like 
`UInt8`
+/// instead of `GDT_Byte`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum GdalDataType {
+    Unknown,
+    UInt8,
+    Int8,
+    UInt16,
+    Int16,
+    UInt32,
+    Int32,
+    UInt64,
+    Int64,
+    Float32,
+    Float64,
+}
+
+impl GdalDataType {
+    /// Convert from the C-level `GDALDataType` enum.
+    ///
+    /// Returns `None` for complex types and `GDT_TypeCount`.
+    pub fn from_c(c_type: GDALDataType) -> Option<Self> {
+        match c_type {
+            GDALDataType::GDT_Unknown => Some(Self::Unknown),
+            GDALDataType::GDT_Byte => Some(Self::UInt8),
+            GDALDataType::GDT_Int8 => Some(Self::Int8),
+            GDALDataType::GDT_UInt16 => Some(Self::UInt16),
+            GDALDataType::GDT_Int16 => Some(Self::Int16),
+            GDALDataType::GDT_UInt32 => Some(Self::UInt32),
+            GDALDataType::GDT_Int32 => Some(Self::Int32),
+            GDALDataType::GDT_UInt64 => Some(Self::UInt64),
+            GDALDataType::GDT_Int64 => Some(Self::Int64),
+            GDALDataType::GDT_Float32 => Some(Self::Float32),
+            GDALDataType::GDT_Float64 => Some(Self::Float64),
+            _ => None, // Complex types, Float16, TypeCount
+        }
+    }
+
+    /// Convert to the C-level `GDALDataType` enum.
+    pub fn to_c(self) -> GDALDataType {
+        match self {
+            Self::Unknown => GDALDataType::GDT_Unknown,
+            Self::UInt8 => GDALDataType::GDT_Byte,
+            Self::Int8 => GDALDataType::GDT_Int8,
+            Self::UInt16 => GDALDataType::GDT_UInt16,
+            Self::Int16 => GDALDataType::GDT_Int16,
+            Self::UInt32 => GDALDataType::GDT_UInt32,
+            Self::Int32 => GDALDataType::GDT_Int32,
+            Self::UInt64 => GDALDataType::GDT_UInt64,
+            Self::Int64 => GDALDataType::GDT_Int64,
+            Self::Float32 => GDALDataType::GDT_Float32,
+            Self::Float64 => GDALDataType::GDT_Float64,
+        }
+    }
+
+    /// Return the ordinal value compatible with the C API (same as 
`self.to_c() as i32`).
+    pub fn ordinal(self) -> i32 {
+        self.to_c() as i32
+    }
+
+    /// Return the byte size of this data type (0 for Unknown).
+    pub fn byte_size(self) -> usize {
+        match self {
+            Self::Unknown => 0,
+            Self::UInt8 | Self::Int8 => 1,
+            Self::UInt16 | Self::Int16 => 2,
+            Self::UInt32 | Self::Int32 | Self::Float32 => 4,
+            Self::UInt64 | Self::Int64 | Self::Float64 => 8,
+        }
+    }
+}
+
+/// Trait mapping Rust primitive types to GDAL data types.
+pub trait GdalType {
+    fn gdal_ordinal() -> GDALDataType;
+}
+
+macro_rules! impl_gdal_type {
+    ($($ty:ty => $variant:ident),+ $(,)?) => {
+        $(
+            impl GdalType for $ty {
+                fn gdal_ordinal() -> GDALDataType {
+                    GDALDataType::$variant
+                }
+            }
+        )+
+    };
+}
+
+impl_gdal_type! {
+    u8 => GDT_Byte,
+    i8 => GDT_Int8,
+    u16 => GDT_UInt16,
+    i16 => GDT_Int16,
+    u32 => GDT_UInt32,
+    i32 => GDT_Int32,
+    u64 => GDT_UInt64,
+    i64 => GDT_Int64,
+    f32 => GDT_Float32,
+    f64 => GDT_Float64,
+}
+
+/// A 2D raster buffer.
+#[derive(Debug, Clone)]
+pub struct Buffer<T: GdalType> {
+    /// Shape as (cols, rows) — matches georust/gdal convention.
+    pub shape: (usize, usize),
+    pub data: Vec<T>,
+}
+
+impl<T: GdalType + Copy> Buffer<T> {
+    pub fn new(shape: (usize, usize), data: Vec<T>) -> Self {
+        Self { shape, data }
+    }
+
+    /// Return the buffer data as a slice (georust compatibility).
+    pub fn data(&self) -> &[T] {
+        &self.data
+    }
+}
+
+/// Options for opening a dataset.
+pub struct DatasetOptions<'a> {
+    pub open_flags: GDALOpenFlags,
+    pub allowed_drivers: Option<&'a [&'a str]>,
+    pub open_options: Option<&'a [&'a str]>,
+    pub sibling_files: Option<&'a [&'a str]>,
+}
+
+impl<'a> Default for DatasetOptions<'a> {
+    fn default() -> Self {
+        Self {
+            open_flags: GDAL_OF_READONLY | GDAL_OF_VERBOSE_ERROR,
+            allowed_drivers: None,
+            open_options: None,
+            sibling_files: None,
+        }
+    }
+}
+
+/// Raster creation options (list of "KEY=VALUE" strings).
+pub type RasterCreationOptions<'a> = &'a [&'a str];
+
+/// GDAL resample algorithm.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ResampleAlg {
+    NearestNeighbour,
+    Bilinear,
+    Cubic,
+    CubicSpline,
+    Lanczos,
+    Average,
+    Mode,
+    Gauss,
+}
+
+impl ResampleAlg {
+    /// Convert to the numeric `GDALRIOResampleAlg` value used by 
`GDALRasterIOExtraArg`.
+    pub fn to_gdal(self) -> GDALRIOResampleAlg {
+        match self {
+            ResampleAlg::NearestNeighbour => 
gdal_dyn_bindgen::GRIORA_NearestNeighbour,
+            ResampleAlg::Bilinear => gdal_dyn_bindgen::GRIORA_Bilinear,
+            ResampleAlg::Cubic => gdal_dyn_bindgen::GRIORA_Cubic,
+            ResampleAlg::CubicSpline => gdal_dyn_bindgen::GRIORA_CubicSpline,
+            ResampleAlg::Lanczos => gdal_dyn_bindgen::GRIORA_Lanczos,
+            ResampleAlg::Average => gdal_dyn_bindgen::GRIORA_Average,
+            ResampleAlg::Mode => gdal_dyn_bindgen::GRIORA_Mode,
+            ResampleAlg::Gauss => gdal_dyn_bindgen::GRIORA_Gauss,
+        }
+    }
+
+    /// Return the string name for use in overview building and VRT resampling 
options.
+    pub fn to_gdal_str(self) -> &'static str {
+        match self {
+            ResampleAlg::NearestNeighbour => "NearestNeighbour",
+            ResampleAlg::Bilinear => "Bilinear",
+            ResampleAlg::Cubic => "Cubic",
+            ResampleAlg::CubicSpline => "CubicSpline",
+            ResampleAlg::Lanczos => "Lanczos",
+            ResampleAlg::Average => "Average",
+            ResampleAlg::Mode => "Mode",
+            ResampleAlg::Gauss => "Gauss",
+        }
+    }
+}


Reply via email to