scuffle_ffmpeg/
dict.rs

1use core::ffi::CStr;
2use std::borrow::Cow;
3use std::ffi::CString;
4use std::ptr::NonNull;
5
6use crate::error::{FfmpegError, FfmpegErrorCode};
7use crate::ffi::*;
8use crate::smart_object::SmartPtr;
9use crate::AVDictionaryFlags;
10
11/// A dictionary of key-value pairs.
12pub struct Dictionary {
13    ptr: SmartPtr<AVDictionary>,
14}
15
16/// Safety: `Dictionary` is safe to send between threads.
17unsafe impl Send for Dictionary {}
18
19impl Default for Dictionary {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl std::fmt::Debug for Dictionary {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        let mut map = f.debug_map();
28
29        for (key, value) in self.iter() {
30            map.entry(&key, &value);
31        }
32
33        map.finish()
34    }
35}
36
37impl Clone for Dictionary {
38    fn clone(&self) -> Self {
39        let mut dict = Self::new();
40
41        Self::clone_from(&mut dict, self);
42
43        dict
44    }
45
46    fn clone_from(&mut self, source: &Self) {
47        // Safety: av_dict_copy is safe to call
48        FfmpegErrorCode::from(unsafe { av_dict_copy(self.as_mut_ptr_ref(), source.as_ptr(), 0) })
49            .result()
50            .expect("Failed to clone dictionary");
51    }
52}
53
54/// A trait for types that can be converted to a `CStr`.
55///
56/// This is used to allow for a few different types:
57/// - [`&str`] - Will be copied and converted to a `CString`.
58/// - [`CStr`] - Will be borrowed.
59/// - [`String`] - Will be copied and converted to a `CString`.
60/// - [`CString`] - Will be owned.
61///
62/// If the string is empty, the [`Option::None`] will be returned.
63///
64/// # Examples
65///
66/// ```rust
67/// use scuffle_ffmpeg::dict::Dictionary;
68///
69/// let mut dict = Dictionary::new();
70///
71/// // "key" is a &CStr, so it will be borrowed.
72/// dict.set(c"key", c"value").expect("Failed to set key");
73///
74/// // "key" is a &str, so it will be copied and converted to a CString.
75/// assert_eq!(dict.get("key"), Some(c"value"));
76///
77/// // "nonexistent_key" is a &str, so it will be copied and converted to a CString.
78/// assert_eq!(dict.set("nonexistent_key".to_owned(), "value"), Ok(()));
79///
80/// // "nonexistent_key" is a CString, so it will be borrowed.
81/// assert_eq!(dict.get(c"nonexistent_key".to_owned()), Some(c"value"));
82/// ```
83pub trait CStringLike<'a> {
84    /// Convert the type to a `CStr`.
85    fn into_c_str(self) -> Option<Cow<'a, CStr>>;
86}
87
88impl<'a> CStringLike<'a> for String {
89    fn into_c_str(self) -> Option<Cow<'a, CStr>> {
90        if self.is_empty() {
91            return None;
92        }
93
94        Some(Cow::Owned(CString::new(Vec::from(self)).ok()?))
95    }
96}
97
98impl<'a> CStringLike<'a> for &str {
99    fn into_c_str(self) -> Option<Cow<'a, CStr>> {
100        if self.is_empty() {
101            return None;
102        }
103
104        Some(Cow::Owned(CString::new(self.as_bytes().to_vec()).ok()?))
105    }
106}
107
108impl<'a> CStringLike<'a> for &'a CStr {
109    fn into_c_str(self) -> Option<Cow<'a, CStr>> {
110        if self.is_empty() {
111            return None;
112        }
113
114        Some(Cow::Borrowed(self))
115    }
116}
117
118impl<'a> CStringLike<'a> for CString {
119    fn into_c_str(self) -> Option<Cow<'a, CStr>> {
120        if self.is_empty() {
121            return None;
122        }
123
124        Some(Cow::Owned(self))
125    }
126}
127
128impl Dictionary {
129    /// Creates a new dictionary.
130    pub const fn new() -> Self {
131        Self {
132            // Safety: A null pointer is a valid dictionary, and a valid pointer.
133            ptr: SmartPtr::null(|ptr| {
134                // Safety: av_dict_free is safe to call
135                unsafe { av_dict_free(ptr) }
136            }),
137        }
138    }
139
140    /// Wrap a pointer to a [`AVDictionary`] in a [`Dictionary`].
141    /// Without taking ownership of the dictionary.
142    /// # Safety
143    /// `ptr` must be a valid pointer.
144    /// The caller must also ensure that the dictionary is not freed while this
145    /// object is alive, and that we don't use the pointer as mutable
146    pub const unsafe fn from_ptr_ref(ptr: *mut AVDictionary) -> Self {
147        // We don't own the dictionary, so we don't need to free it
148        Self {
149            ptr: SmartPtr::wrap(ptr as _, |_| {}),
150        }
151    }
152
153    /// Wrap a pointer to a [`AVDictionary`] in a [`Dictionary`].
154    /// Takes ownership of the dictionary.
155    /// Meaning it will be freed when the [`Dictionary`] is dropped.
156    /// # Safety
157    /// `ptr` must be a valid pointer.
158    pub const unsafe fn from_ptr_owned(ptr: *mut AVDictionary) -> Self {
159        Self {
160            ptr: SmartPtr::wrap(ptr, |ptr| {
161                // Safety: av_dict_free is safe to call
162                av_dict_free(ptr)
163            }),
164        }
165    }
166
167    /// Sets a key-value pair in the dictionary.
168    /// Key and value must not be empty.
169    pub fn set<'a>(&mut self, key: impl CStringLike<'a>, value: impl CStringLike<'a>) -> Result<(), FfmpegError> {
170        let key = key.into_c_str().ok_or(FfmpegError::Arguments("key cannot be empty"))?;
171        let value = value.into_c_str().ok_or(FfmpegError::Arguments("value cannot be empty"))?;
172
173        // Safety: av_dict_set is safe to call
174        FfmpegErrorCode(unsafe { av_dict_set(self.ptr.as_mut(), key.as_ptr(), value.as_ptr(), 0) }).result()?;
175        Ok(())
176    }
177
178    /// Returns the value associated with the given key.
179    /// If the key is not found, the [`Option::None`] will be returned.
180    pub fn get<'a>(&self, key: impl CStringLike<'a>) -> Option<&CStr> {
181        let key = key.into_c_str()?;
182
183        let mut entry =
184            // Safety: av_dict_get is safe to call
185            NonNull::new(unsafe { av_dict_get(self.as_ptr(), key.as_ptr(), std::ptr::null_mut(), AVDictionaryFlags::IgnoreSuffix.into()) })?;
186
187        // Safety: The pointer here is valid.
188        let mut_ref = unsafe { entry.as_mut() };
189
190        // Safety: The pointer here is valid.
191        Some(unsafe { CStr::from_ptr(mut_ref.value) })
192    }
193
194    /// Returns true if the dictionary is empty.
195    pub fn is_empty(&self) -> bool {
196        self.iter().next().is_none()
197    }
198
199    /// Returns an iterator over the dictionary.
200    pub const fn iter(&self) -> DictionaryIterator {
201        DictionaryIterator::new(self)
202    }
203
204    /// Returns the pointer to the dictionary.
205    pub const fn as_ptr(&self) -> *const AVDictionary {
206        self.ptr.as_ptr()
207    }
208
209    /// Returns a mutable reference to the pointer to the dictionary.
210    pub const fn as_mut_ptr_ref(&mut self) -> &mut *mut AVDictionary {
211        self.ptr.as_mut()
212    }
213
214    /// Returns the pointer to the dictionary.
215    pub fn leak(self) -> *mut AVDictionary {
216        self.ptr.into_inner()
217    }
218
219    /// Extends a dictionary with an iterator of key-value pairs.
220    pub fn extend<'a, K, V>(&mut self, iter: impl IntoIterator<Item = (K, V)>) -> Result<(), FfmpegError>
221    where
222        K: CStringLike<'a>,
223        V: CStringLike<'a>,
224    {
225        for (key, value) in iter {
226            // This is less then ideal, we shouldnt ignore the error but it only happens if the key or value is empty.
227            self.set(key, value)?;
228        }
229
230        Ok(())
231    }
232
233    /// Creates a new dictionary from an iterator of key-value pairs.
234    pub fn try_from_iter<'a, K, V>(iter: impl IntoIterator<Item = (K, V)>) -> Result<Self, FfmpegError>
235    where
236        K: CStringLike<'a>,
237        V: CStringLike<'a>,
238    {
239        let mut dict = Self::new();
240        dict.extend(iter)?;
241        Ok(dict)
242    }
243}
244
245/// An iterator over the dictionary.
246pub struct DictionaryIterator<'a> {
247    dict: &'a Dictionary,
248    entry: *mut AVDictionaryEntry,
249}
250
251impl<'a> DictionaryIterator<'a> {
252    /// Creates a new dictionary iterator.
253    const fn new(dict: &'a Dictionary) -> Self {
254        Self {
255            dict,
256            entry: std::ptr::null_mut(),
257        }
258    }
259}
260
261impl<'a> Iterator for DictionaryIterator<'a> {
262    type Item = (&'a CStr, &'a CStr);
263
264    fn next(&mut self) -> Option<Self::Item> {
265        // Safety: av_dict_get is safe to call
266        self.entry = unsafe {
267            av_dict_get(
268                self.dict.as_ptr(),
269                // ffmpeg expects a null terminated string when iterating over the dictionary entries.
270                &[0i8] as *const libc::c_char,
271                self.entry,
272                AVDictionaryFlags::IgnoreSuffix.into(),
273            )
274        };
275
276        let mut entry = NonNull::new(self.entry)?;
277
278        // Safety: The pointer here is valid.
279        let entry_ref = unsafe { entry.as_mut() };
280
281        // Safety: The pointer here is valid.
282        let key = unsafe { CStr::from_ptr(entry_ref.key) };
283        // Safety: The pointer here is valid.
284        let value = unsafe { CStr::from_ptr(entry_ref.value) };
285
286        Some((key, value))
287    }
288}
289
290impl<'a> IntoIterator for &'a Dictionary {
291    type IntoIter = DictionaryIterator<'a>;
292    type Item = <DictionaryIterator<'a> as Iterator>::Item;
293
294    fn into_iter(self) -> Self::IntoIter {
295        DictionaryIterator::new(self)
296    }
297}
298
299#[cfg(test)]
300#[cfg_attr(all(test, coverage_nightly), coverage(off))]
301mod tests {
302
303    use std::collections::HashMap;
304    use std::ffi::CStr;
305
306    use crate::dict::Dictionary;
307
308    fn sort_hashmap<K: Ord, V>(map: std::collections::HashMap<K, V>) -> std::collections::BTreeMap<K, V> {
309        map.into_iter().collect()
310    }
311
312    #[test]
313    fn test_dict_default_and_items() {
314        let mut dict = Dictionary::default();
315
316        assert!(dict.is_empty(), "Default dictionary should be empty");
317        assert!(dict.as_ptr().is_null(), "Default dictionary pointer should be null");
318
319        dict.set(c"key1", c"value1").expect("Failed to set key1");
320        dict.set(c"key2", c"value2").expect("Failed to set key2");
321        dict.set(c"key3", c"value3").expect("Failed to set key3");
322
323        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
324
325        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
326        {
327            "key1": "value1",
328            "key2": "value2",
329            "key3": "value3",
330        }
331        "#);
332    }
333
334    #[test]
335    fn test_dict_set_empty_key() {
336        let mut dict = Dictionary::new();
337        assert!(dict.set(c"", c"value1").is_err());
338    }
339
340    #[test]
341    fn test_dict_clone_empty() {
342        let empty_dict = Dictionary::new();
343        let cloned_dict = empty_dict.clone();
344
345        assert!(cloned_dict.is_empty(), "Cloned dictionary should be empty");
346        assert!(empty_dict.is_empty(), "Original dictionary should remain empty");
347    }
348
349    #[test]
350    fn test_dict_clone_non_empty() {
351        let mut dict = Dictionary::new();
352        dict.set(c"key1", c"value1").expect("Failed to set key1");
353        dict.set(c"key2", c"value2").expect("Failed to set key2");
354        let mut clone = dict.clone();
355
356        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
357        let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
358
359        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
360        {
361            "key1": "value1",
362            "key2": "value2",
363        }
364        "#);
365        insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
366        {
367            "key1": "value1",
368            "key2": "value2",
369        }
370        "#);
371
372        clone
373            .set(c"key3", c"value3")
374            .expect("Failed to set key3 in cloned dictionary");
375
376        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
377        let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
378        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
379        {
380            "key1": "value1",
381            "key2": "value2",
382        }
383        "#);
384        insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
385        {
386            "key1": "value1",
387            "key2": "value2",
388            "key3": "value3",
389        }
390        "#);
391    }
392
393    #[test]
394    fn test_dict_get() {
395        let mut dict = Dictionary::new();
396        assert!(
397            dict.get(c"nonexistent_key").is_none(),
398            "Getting a nonexistent key from an empty dictionary should return None"
399        );
400
401        dict.set(c"key1", c"value1").expect("Failed to set key1");
402        dict.set(c"key2", c"value2").expect("Failed to set key2");
403        assert_eq!(dict.get(c"key1"), Some(c"value1"), "The value for 'key1' should be 'value1'");
404        assert_eq!(dict.get(c"key2"), Some(c"value2"), "The value for 'key2' should be 'value2'");
405
406        assert!(dict.get(c"key3").is_none(), "Getting a nonexistent key should return None");
407
408        dict.set(c"special_key!", c"special_value")
409            .expect("Failed to set special_key!");
410        assert_eq!(
411            dict.get(c"special_key!"),
412            Some(c"special_value"),
413            "The value for 'special_key!' should be 'special_value'"
414        );
415
416        assert!(
417            dict.get(c"").is_none(),
418            "Getting an empty key should return None (empty keys are not allowed)"
419        );
420    }
421
422    #[test]
423    fn test_from_hashmap_for_dictionary() {
424        let mut hash_map = std::collections::HashMap::new();
425        hash_map.insert("key1".to_string(), "value1".to_string());
426        hash_map.insert("key2".to_string(), "value2".to_string());
427        hash_map.insert("key3".to_string(), "value3".to_string());
428        let dict = Dictionary::try_from_iter(hash_map).expect("Failed to create dictionary from hashmap");
429
430        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
431        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
432        {
433            "key1": "value1",
434            "key2": "value2",
435            "key3": "value3",
436        }
437        "#);
438    }
439
440    #[test]
441    fn test_empty_string() {
442        let mut dict = Dictionary::new();
443        assert!(dict.set(c"", c"abc").is_err());
444        assert!(dict.set(c"abc", c"").is_err());
445        assert!(dict.get(c"").is_none());
446        assert!(dict.set("".to_owned(), "abc".to_owned()).is_err());
447        assert!(dict.set("abc".to_owned(), "".to_owned()).is_err());
448        assert!(dict.get("").is_none());
449        assert!(dict.set(c"".to_owned(), c"abc".to_owned()).is_err());
450        assert!(dict.set(c"abc".to_owned(), c"".to_owned()).is_err());
451        assert!(dict.get(c"").is_none());
452    }
453}