scuffle_ffmpeg/
dict.rs

1use core::ffi::CStr;
2use std::borrow::Cow;
3use std::ffi::CString;
4use std::ptr::NonNull;
5
6use crate::AVDictionaryFlags;
7use crate::error::{FfmpegError, FfmpegErrorCode};
8use crate::ffi::*;
9use crate::smart_object::SmartPtr;
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        Self {
148            // Safety: The safety comment of the function implies this is safe.
149            // We don't own the dictionary, so we don't need to free it
150            ptr: unsafe { SmartPtr::wrap(ptr as _, |_| {}) },
151        }
152    }
153
154    /// Wrap a pointer to a [`AVDictionary`] in a [`Dictionary`].
155    /// Takes ownership of the dictionary.
156    /// Meaning it will be freed when the [`Dictionary`] is dropped.
157    /// # Safety
158    /// `ptr` must be a valid pointer.
159    pub const unsafe fn from_ptr_owned(ptr: *mut AVDictionary) -> Self {
160        let destructor = |ptr: &mut *mut AVDictionary| {
161            // Safety: av_dict_free is safe to call & we own the pointer.
162            unsafe { av_dict_free(ptr) }
163        };
164
165        Self {
166            // Safety: The safety comment of the function implies this is safe.
167            ptr: unsafe { SmartPtr::wrap(ptr, destructor) },
168        }
169    }
170
171    /// Sets a key-value pair in the dictionary.
172    /// Key and value must not be empty.
173    pub fn set<'a>(&mut self, key: impl CStringLike<'a>, value: impl CStringLike<'a>) -> Result<(), FfmpegError> {
174        let key = key.into_c_str().ok_or(FfmpegError::Arguments("key cannot be empty"))?;
175        let value = value.into_c_str().ok_or(FfmpegError::Arguments("value cannot be empty"))?;
176
177        // Safety: av_dict_set is safe to call
178        FfmpegErrorCode(unsafe { av_dict_set(self.ptr.as_mut(), key.as_ptr() as *const _, value.as_ptr() as *const _, 0) })
179            .result()?;
180        Ok(())
181    }
182
183    /// Returns the value associated with the given key.
184    /// If the key is not found, the [`Option::None`] will be returned.
185    pub fn get<'a>(&self, key: impl CStringLike<'a>) -> Option<&CStr> {
186        let key = key.into_c_str()?;
187
188        let mut entry =
189            // Safety: av_dict_get is safe to call
190            NonNull::new(unsafe {
191                av_dict_get(
192                    self.as_ptr(),
193                    key.as_ptr() as *const _,
194                    std::ptr::null_mut(),
195                    AVDictionaryFlags::IgnoreSuffix.into(),
196                )
197            })?;
198
199        // Safety: The pointer here is valid.
200        let mut_ref = unsafe { entry.as_mut() };
201
202        // Safety: The pointer here is valid.
203        Some(unsafe { CStr::from_ptr(mut_ref.value as *const _) })
204    }
205
206    /// Returns true if the dictionary is empty.
207    pub fn is_empty(&self) -> bool {
208        self.iter().next().is_none()
209    }
210
211    /// Returns an iterator over the dictionary.
212    pub const fn iter(&self) -> DictionaryIterator {
213        DictionaryIterator::new(self)
214    }
215
216    /// Returns the pointer to the dictionary.
217    pub const fn as_ptr(&self) -> *const AVDictionary {
218        self.ptr.as_ptr()
219    }
220
221    /// Returns a mutable reference to the pointer to the dictionary.
222    pub const fn as_mut_ptr_ref(&mut self) -> &mut *mut AVDictionary {
223        self.ptr.as_mut()
224    }
225
226    /// Returns the pointer to the dictionary.
227    pub fn leak(self) -> *mut AVDictionary {
228        self.ptr.into_inner()
229    }
230
231    /// Extends a dictionary with an iterator of key-value pairs.
232    pub fn extend<'a, K, V>(&mut self, iter: impl IntoIterator<Item = (K, V)>) -> Result<(), FfmpegError>
233    where
234        K: CStringLike<'a>,
235        V: CStringLike<'a>,
236    {
237        for (key, value) in iter {
238            // This is less then ideal, we shouldnt ignore the error but it only happens if the key or value is empty.
239            self.set(key, value)?;
240        }
241
242        Ok(())
243    }
244
245    /// Creates a new dictionary from an iterator of key-value pairs.
246    pub fn try_from_iter<'a, K, V>(iter: impl IntoIterator<Item = (K, V)>) -> Result<Self, FfmpegError>
247    where
248        K: CStringLike<'a>,
249        V: CStringLike<'a>,
250    {
251        let mut dict = Self::new();
252        dict.extend(iter)?;
253        Ok(dict)
254    }
255}
256
257/// An iterator over the dictionary.
258pub struct DictionaryIterator<'a> {
259    dict: &'a Dictionary,
260    entry: *mut AVDictionaryEntry,
261}
262
263impl<'a> DictionaryIterator<'a> {
264    /// Creates a new dictionary iterator.
265    const fn new(dict: &'a Dictionary) -> Self {
266        Self {
267            dict,
268            entry: std::ptr::null_mut(),
269        }
270    }
271}
272
273impl<'a> Iterator for DictionaryIterator<'a> {
274    type Item = (&'a CStr, &'a CStr);
275
276    fn next(&mut self) -> Option<Self::Item> {
277        // Safety: av_dict_get is safe to call
278        self.entry = unsafe {
279            av_dict_get(
280                self.dict.as_ptr(),
281                // ffmpeg expects a null terminated string when iterating over the dictionary entries.
282                c"".as_ptr() as *const _,
283                self.entry,
284                AVDictionaryFlags::IgnoreSuffix.into(),
285            )
286        };
287
288        let mut entry = NonNull::new(self.entry)?;
289
290        // Safety: The pointer here is valid.
291        let entry_ref = unsafe { entry.as_mut() };
292
293        // Safety: The pointer here is valid.
294        let key = unsafe { CStr::from_ptr(entry_ref.key as *const _) };
295        // Safety: The pointer here is valid.
296        let value = unsafe { CStr::from_ptr(entry_ref.value as *const _) };
297
298        Some((key, value))
299    }
300}
301
302impl<'a> IntoIterator for &'a Dictionary {
303    type IntoIter = DictionaryIterator<'a>;
304    type Item = <DictionaryIterator<'a> as Iterator>::Item;
305
306    fn into_iter(self) -> Self::IntoIter {
307        DictionaryIterator::new(self)
308    }
309}
310
311#[cfg(test)]
312#[cfg_attr(all(test, coverage_nightly), coverage(off))]
313mod tests {
314
315    use std::collections::HashMap;
316    use std::ffi::CStr;
317
318    use crate::dict::Dictionary;
319
320    fn sort_hashmap<K: Ord, V>(map: std::collections::HashMap<K, V>) -> std::collections::BTreeMap<K, V> {
321        map.into_iter().collect()
322    }
323
324    #[test]
325    fn test_dict_default_and_items() {
326        let mut dict = Dictionary::default();
327
328        assert!(dict.is_empty(), "Default dictionary should be empty");
329        assert!(dict.as_ptr().is_null(), "Default dictionary pointer should be null");
330
331        dict.set(c"key1", c"value1").expect("Failed to set key1");
332        dict.set(c"key2", c"value2").expect("Failed to set key2");
333        dict.set(c"key3", c"value3").expect("Failed to set key3");
334
335        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
336
337        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
338        {
339            "key1": "value1",
340            "key2": "value2",
341            "key3": "value3",
342        }
343        "#);
344    }
345
346    #[test]
347    fn test_dict_set_empty_key() {
348        let mut dict = Dictionary::new();
349        assert!(dict.set(c"", c"value1").is_err());
350    }
351
352    #[test]
353    fn test_dict_clone_empty() {
354        let empty_dict = Dictionary::new();
355        let cloned_dict = empty_dict.clone();
356
357        assert!(cloned_dict.is_empty(), "Cloned dictionary should be empty");
358        assert!(empty_dict.is_empty(), "Original dictionary should remain empty");
359    }
360
361    #[test]
362    fn test_dict_clone_non_empty() {
363        let mut dict = Dictionary::new();
364        dict.set(c"key1", c"value1").expect("Failed to set key1");
365        dict.set(c"key2", c"value2").expect("Failed to set key2");
366        let mut clone = dict.clone();
367
368        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
369        let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
370
371        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
372        {
373            "key1": "value1",
374            "key2": "value2",
375        }
376        "#);
377        insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
378        {
379            "key1": "value1",
380            "key2": "value2",
381        }
382        "#);
383
384        clone
385            .set(c"key3", c"value3")
386            .expect("Failed to set key3 in cloned dictionary");
387
388        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
389        let clone_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&clone);
390        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
391        {
392            "key1": "value1",
393            "key2": "value2",
394        }
395        "#);
396        insta::assert_debug_snapshot!(sort_hashmap(clone_hm), @r#"
397        {
398            "key1": "value1",
399            "key2": "value2",
400            "key3": "value3",
401        }
402        "#);
403    }
404
405    #[test]
406    fn test_dict_get() {
407        let mut dict = Dictionary::new();
408        assert!(
409            dict.get(c"nonexistent_key").is_none(),
410            "Getting a nonexistent key from an empty dictionary should return None"
411        );
412
413        dict.set(c"key1", c"value1").expect("Failed to set key1");
414        dict.set(c"key2", c"value2").expect("Failed to set key2");
415        assert_eq!(dict.get(c"key1"), Some(c"value1"), "The value for 'key1' should be 'value1'");
416        assert_eq!(dict.get(c"key2"), Some(c"value2"), "The value for 'key2' should be 'value2'");
417
418        assert!(dict.get(c"key3").is_none(), "Getting a nonexistent key should return None");
419
420        dict.set(c"special_key!", c"special_value")
421            .expect("Failed to set special_key!");
422        assert_eq!(
423            dict.get(c"special_key!"),
424            Some(c"special_value"),
425            "The value for 'special_key!' should be 'special_value'"
426        );
427
428        assert!(
429            dict.get(c"").is_none(),
430            "Getting an empty key should return None (empty keys are not allowed)"
431        );
432    }
433
434    #[test]
435    fn test_from_hashmap_for_dictionary() {
436        let mut hash_map = std::collections::HashMap::new();
437        hash_map.insert("key1".to_string(), "value1".to_string());
438        hash_map.insert("key2".to_string(), "value2".to_string());
439        hash_map.insert("key3".to_string(), "value3".to_string());
440        let dict = Dictionary::try_from_iter(hash_map).expect("Failed to create dictionary from hashmap");
441
442        let dict_hm: std::collections::HashMap<&CStr, &CStr> = HashMap::from_iter(&dict);
443        insta::assert_debug_snapshot!(sort_hashmap(dict_hm), @r#"
444        {
445            "key1": "value1",
446            "key2": "value2",
447            "key3": "value3",
448        }
449        "#);
450    }
451
452    #[test]
453    fn test_empty_string() {
454        let mut dict = Dictionary::new();
455        assert!(dict.set(c"", c"abc").is_err());
456        assert!(dict.set(c"abc", c"").is_err());
457        assert!(dict.get(c"").is_none());
458        assert!(dict.set("".to_owned(), "abc".to_owned()).is_err());
459        assert!(dict.set("abc".to_owned(), "".to_owned()).is_err());
460        assert!(dict.get("").is_none());
461        assert!(dict.set(c"".to_owned(), c"abc".to_owned()).is_err());
462        assert!(dict.set(c"abc".to_owned(), c"".to_owned()).is_err());
463        assert!(dict.get(c"").is_none());
464    }
465}