use core::ffi::CStr;
use std::borrow::Cow;
use std::ffi::CString;
use std::ptr::NonNull;
use crate::AVDictionaryFlags;
use crate::error::{FfmpegError, FfmpegErrorCode};
use crate::ffi::*;
use crate::smart_object::SmartPtr;
pub struct Dictionary {
ptr: SmartPtr<AVDictionary>,
}
unsafe impl Send for Dictionary {}
impl Default for Dictionary {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Dictionary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut map = f.debug_map();
for (key, value) in self.iter() {
map.entry(&key, &value);
}
map.finish()
}
}
impl Clone for Dictionary {
fn clone(&self) -> Self {
let mut dict = Self::new();
Self::clone_from(&mut dict, self);
dict
}
fn clone_from(&mut self, source: &Self) {
FfmpegErrorCode::from(unsafe { av_dict_copy(self.as_mut_ptr_ref(), source.as_ptr(), 0) })
.result()
.expect("Failed to clone dictionary");
}
}
pub trait CStringLike<'a> {
fn into_c_str(self) -> Option<Cow<'a, CStr>>;
}
impl<'a> CStringLike<'a> for String {
fn into_c_str(self) -> Option<Cow<'a, CStr>> {
if self.is_empty() {
return None;
}
Some(Cow::Owned(CString::new(Vec::from(self)).ok()?))
}
}
impl<'a> CStringLike<'a> for &str {
fn into_c_str(self) -> Option<Cow<'a, CStr>> {
if self.is_empty() {
return None;
}
Some(Cow::Owned(CString::new(self.as_bytes().to_vec()).ok()?))
}
}
impl<'a> CStringLike<'a> for &'a CStr {
fn into_c_str(self) -> Option<Cow<'a, CStr>> {
if self.is_empty() {
return None;
}
Some(Cow::Borrowed(self))
}
}
impl<'a> CStringLike<'a> for CString {
fn into_c_str(self) -> Option<Cow<'a, CStr>> {
if self.is_empty() {
return None;
}
Some(Cow::Owned(self))
}
}
impl Dictionary {
pub const fn new() -> Self {
Self {
ptr: SmartPtr::null(|ptr| {
unsafe { av_dict_free(ptr) }
}),
}
}
pub const unsafe fn from_ptr_ref(ptr: *mut AVDictionary) -> Self {
Self {
ptr: unsafe { SmartPtr::wrap(ptr as _, |_| {}) },
}
}
pub const unsafe fn from_ptr_owned(ptr: *mut AVDictionary) -> Self {
let destructor = |ptr: &mut *mut AVDictionary| {
unsafe { av_dict_free(ptr) }
};
Self {
ptr: unsafe { SmartPtr::wrap(ptr, destructor) },
}
}
pub fn set<'a>(&mut self, key: impl CStringLike<'a>, value: impl CStringLike<'a>) -> Result<(), FfmpegError> {
let key = key.into_c_str().ok_or(FfmpegError::Arguments("key cannot be empty"))?;
let value = value.into_c_str().ok_or(FfmpegError::Arguments("value cannot be empty"))?;
FfmpegErrorCode(unsafe { av_dict_set(self.ptr.as_mut(), key.as_ptr() as *const _, value.as_ptr() as *const _, 0) })
.result()?;
Ok(())
}
pub fn get<'a>(&self, key: impl CStringLike<'a>) -> Option<&CStr> {
let key = key.into_c_str()?;
let mut entry =
NonNull::new(unsafe {
av_dict_get(
self.as_ptr(),
key.as_ptr() as *const _,
std::ptr::null_mut(),
AVDictionaryFlags::IgnoreSuffix.into(),
)
})?;
let mut_ref = unsafe { entry.as_mut() };
Some(unsafe { CStr::from_ptr(mut_ref.value as *const _) })
}
pub fn is_empty(&self) -> bool {
self.iter().next().is_none()
}
pub const fn iter(&self) -> DictionaryIterator<'_> {
DictionaryIterator::new(self)
}
pub const fn as_ptr(&self) -> *const AVDictionary {
self.ptr.as_ptr()
}
pub const fn as_mut_ptr_ref(&mut self) -> &mut *mut AVDictionary {
self.ptr.as_mut()
}
pub fn leak(self) -> *mut AVDictionary {
self.ptr.into_inner()
}
pub fn extend<'a, K, V>(&mut self, iter: impl IntoIterator<Item = (K, V)>) -> Result<(), FfmpegError>
where
K: CStringLike<'a>,
V: CStringLike<'a>,
{
for (key, value) in iter {
self.set(key, value)?;
}
Ok(())
}
pub fn try_from_iter<'a, K, V>(iter: impl IntoIterator<Item = (K, V)>) -> Result<Self, FfmpegError>
where
K: CStringLike<'a>,
V: CStringLike<'a>,
{
let mut dict = Self::new();
dict.extend(iter)?;
Ok(dict)
}
}
pub struct DictionaryIterator<'a> {
dict: &'a Dictionary,
entry: *mut AVDictionaryEntry,
}
impl<'a> DictionaryIterator<'a> {
const fn new(dict: &'a Dictionary) -> Self {
Self {
dict,
entry: std::ptr::null_mut(),
}
}
}
impl<'a> Iterator for DictionaryIterator<'a> {
type Item = (&'a CStr, &'a CStr);
fn next(&mut self) -> Option<Self::Item> {
self.entry = unsafe {
av_dict_get(
self.dict.as_ptr(),
c"".as_ptr() as *const _,
self.entry,
AVDictionaryFlags::IgnoreSuffix.into(),
)
};
let mut entry = NonNull::new(self.entry)?;
let entry_ref = unsafe { entry.as_mut() };
let key = unsafe { CStr::from_ptr(entry_ref.key as *const _) };
let value = unsafe { CStr::from_ptr(entry_ref.value as *const _) };
Some((key, value))
}
}
impl<'a> IntoIterator for &'a Dictionary {
type IntoIter = DictionaryIterator<'a>;
type Item = <DictionaryIterator<'a> as Iterator>::Item;
fn into_iter(self) -> Self::IntoIter {
DictionaryIterator::new(self)
}
}
#[cfg(test)]
#[cfg_attr(all(test, coverage_nightly), coverage(off))]
mod tests {
use std::collections::HashMap;
use std::ffi::CStr;
use crate::dict::Dictionary;
fn sort_hashmap<K: Ord, V>(map: std::collections::HashMap<K, V>) -> std::collections::BTreeMap<K, V> {
map.into_iter().collect()
}
#[test]
fn test_dict_default_and_items() {
let mut dict = Dictionary::default();
assert!(dict.is_empty(), "Default dictionary should be empty");
assert!(dict.as_ptr().is_null(), "Default dictionary pointer should be null");
dict.set(c"key1", c"value1").expect("Failed to set key1");
dict.set(c"key2", c"value2").expect("Failed to set key2");
dict.set(c"key3", c"value3").expect("Failed to set key3");
let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
"#);
}
#[test]
fn test_dict_set_empty_key() {
let mut dict = Dictionary::new();
assert!(dict.set(c"", c"value1").is_err());
}
#[test]
fn test_dict_clone_empty() {
let empty_dict = Dictionary::new();
let cloned_dict = empty_dict.clone();
assert!(cloned_dict.is_empty(), "Cloned dictionary should be empty");
assert!(empty_dict.is_empty(), "Original dictionary should remain empty");
}
#[test]
fn test_dict_clone_non_empty() {
let mut dict = Dictionary::new();
dict.set(c"key1", c"value1").expect("Failed to set key1");
dict.set(c"key2", c"value2").expect("Failed to set key2");
let mut clone = dict.clone();
let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
{
"key1": "value1",
"key2": "value2",
}
"#);
insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
{
"key1": "value1",
"key2": "value2",
}
"#);
clone
.set(c"key3", c"value3")
.expect("Failed to set key3 in cloned dictionary");
let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
{
"key1": "value1",
"key2": "value2",
}
"#);
insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
"#);
}
#[test]
fn test_dict_get() {
let mut dict = Dictionary::new();
assert!(
dict.get(c"nonexistent_key").is_none(),
"Getting a nonexistent key from an empty dictionary should return None"
);
dict.set(c"key1", c"value1").expect("Failed to set key1");
dict.set(c"key2", c"value2").expect("Failed to set key2");
assert_eq!(dict.get(c"key1"), Some(c"value1"), "The value for 'key1' should be 'value1'");
assert_eq!(dict.get(c"key2"), Some(c"value2"), "The value for 'key2' should be 'value2'");
assert!(dict.get(c"key3").is_none(), "Getting a nonexistent key should return None");
dict.set(c"special_key!", c"special_value")
.expect("Failed to set special_key!");
assert_eq!(
dict.get(c"special_key!"),
Some(c"special_value"),
"The value for 'special_key!' should be 'special_value'"
);
assert!(
dict.get(c"").is_none(),
"Getting an empty key should return None (empty keys are not allowed)"
);
}
#[test]
fn test_from_hashmap_for_dictionary() {
let mut hash_map = std::collections::HashMap::new();
hash_map.insert("key1".to_string(), "value1".to_string());
hash_map.insert("key2".to_string(), "value2".to_string());
hash_map.insert("key3".to_string(), "value3".to_string());
let dict = Dictionary::try_from_iter(hash_map).expect("Failed to create dictionary from hashmap");
let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
"#);
}
#[test]
fn test_empty_string() {
let mut dict = Dictionary::new();
assert!(dict.set(c"", c"abc").is_err());
assert!(dict.set(c"abc", c"").is_err());
assert!(dict.get(c"").is_none());
assert!(dict.set("".to_owned(), "abc".to_owned()).is_err());
assert!(dict.set("abc".to_owned(), "".to_owned()).is_err());
assert!(dict.get("").is_none());
assert!(dict.set(c"".to_owned(), c"abc".to_owned()).is_err());
assert!(dict.set(c"abc".to_owned(), c"".to_owned()).is_err());
assert!(dict.get(c"").is_none());
}
}