Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quality-of-life additions to CplStringList. #311

Merged
merged 5 commits into from Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Expand Up @@ -19,6 +19,10 @@

- <https://github.com/georust/gdal/pull/308>

- Added quality-of-life features to `CslStringList`: `len`, `is_empty`, `Debug` and `Iterator` implementations.

- <https://github.com/georust/gdal/pull/311>

## 0.13

- Add prebuild bindings for GDAL 3.5
Expand Down
160 changes: 151 additions & 9 deletions src/cpl.rs
Expand Up @@ -4,18 +4,18 @@
//!

use std::ffi::CString;
use std::fmt::{Debug, Formatter};
use std::ptr;

use gdal_sys::CSLSetNameValue;
use gdal_sys::{CSLCount, CSLDestroy, CSLFetchNameValue, CSLGetField, CSLSetNameValue};
use libc::c_char;

use crate::errors::{GdalError, Result};
use crate::utils::_string;
use crate::utils::{_string, _string_tuple};

/// Wraps a `char **papszStrList` pointer into a struct that
/// automatically destroys the allocated memory on `drop`.
/// Wraps a [`gdal_sys::CSLConstList`] (a.k.a. `char **papszStrList`)
///
/// See the `CSL` GDAL functions for more details.
/// See the [`CSL*` GDAL functions](https://gdal.org/api/cpl.html#cpl-string-h) for more details.
pub struct CslStringList {
list_ptr: *mut *mut c_char,
}
Expand All @@ -30,6 +30,9 @@ impl CslStringList {
/// Assigns `value` to `name`.
///
/// Overwrites duplicate `name`s.
///
/// Returns `Ok<()>` on success, `Err<GdalError>` if `name` has non alphanumeric
/// characters, or `value` has newline characters.
pub fn set_name_value(&mut self, name: &str, value: &str) -> Result<()> {
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(GdalError::BadArgument(format!(
Expand All @@ -53,12 +56,13 @@ impl CslStringList {
Ok(())
}

/// Looks up the value corresponding to a key.
/// Looks up the value corresponding to `key`.
///
/// See `CSLFetchNameValue` for details.
/// See [`CSLFetchNameValue`](https://gdal.org/doxygen/cpl__string_8h.html#a4f23675f8b6f015ed23d9928048361a1)
/// for details.
pub fn fetch_name_value(&self, key: &str) -> Result<Option<String>> {
let key = CString::new(key)?;
let c_value = unsafe { gdal_sys::CSLFetchNameValue(self.as_ptr(), key.as_ptr()) };
let c_value = unsafe { CSLFetchNameValue(self.as_ptr(), key.as_ptr()) };
let value = if c_value.is_null() {
None
} else {
Expand All @@ -67,14 +71,30 @@ impl CslStringList {
Ok(value)
}

/// Determine the number of entries in the list.
pub fn len(&self) -> isize {
unsafe { CSLCount(self.as_ptr()) as isize }
metasim marked this conversation as resolved.
Show resolved Hide resolved
}

/// Determine if the list has any values
pub fn is_empty(&self) -> bool {
self.len() <= 0
}

/// Get an iterator over the name/value elements of the list.
pub fn iter(&self) -> CslStringListIterator {
CslStringListIterator::new(self)
}

/// Get the raw pointer behind list's data.
metasim marked this conversation as resolved.
Show resolved Hide resolved
pub fn as_ptr(&self) -> gdal_sys::CSLConstList {
self.list_ptr
}
}

impl Drop for CslStringList {
fn drop(&mut self) {
unsafe { gdal_sys::CSLDestroy(self.list_ptr) }
unsafe { CSLDestroy(self.list_ptr) }
}
}

Expand All @@ -83,3 +103,125 @@ impl Default for CslStringList {
Self::new()
}
}

pub struct CslStringListIterator<'a> {
list: &'a CslStringList,
idx: isize,
count: isize,
}

impl<'a> CslStringListIterator<'a> {
fn new(list: &'a CslStringList) -> Self {
Self {
list,
idx: 0,
count: list.len(),
}
}
fn is_done(&self) -> bool {
self.idx >= self.count
}
}

impl<'a> Iterator for CslStringListIterator<'a> {
type Item = (String, String);

fn next(&mut self) -> Option<Self::Item> {
if self.is_done() {
return None;
}

let field = unsafe { CSLGetField(self.list.as_ptr(), self.idx as libc::c_int) };
metasim marked this conversation as resolved.
Show resolved Hide resolved
if field.is_null() {
None
} else {
self.idx += 1;
_string_tuple(field, '=')
}
}
}

impl Debug for CslStringList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for (k, v) in self.iter() {
f.write_fmt(format_args!("{k}={v}\n"))?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::cpl::CslStringList;
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")?;

Ok(l)
}

#[test]
fn basic_list() -> Result<()> {
let l = fixture()?;
assert!(matches!(l.fetch_name_value("ONE"), Ok(Some(s)) if s == *"1"));
assert!(matches!(l.fetch_name_value("THREE"), Ok(Some(s)) if s == *"3"));
assert!(matches!(l.fetch_name_value("FOO"), Ok(None)));

Ok(())
}

#[test]
fn has_length() -> Result<()> {
let l = fixture()?;
assert_eq!(l.len(), 3);

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".to_string(), "1".to_string())));
assert_eq!(it.next(), Some(("TWO".to_string(), "2".to_string())));
assert_eq!(it.next(), Some(("THREE".to_string(), "3".to_string())));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None);
Ok(())
}

#[test]
fn invalid_keys() -> 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 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"));

Ok(())
}
}
8 changes: 8 additions & 0 deletions src/utils.rs
Expand Up @@ -10,6 +10,14 @@ pub fn _string(raw_ptr: *const c_char) -> String {
c_str.to_string_lossy().into_owned()
}

pub(crate) fn _string_tuple(raw_ptr: *const c_char, delim: char) -> Option<(String, String)> {
let c_str = unsafe { CStr::from_ptr(raw_ptr) };
c_str
.to_string_lossy()
.split_once(delim)
.map(|(k, v)| (k.to_string(), v.to_string()))
metasim marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn _string_array(raw_ptr: *mut *mut c_char) -> Vec<String> {
_convert_raw_ptr_array(raw_ptr, _string)
}
Expand Down