Skip to main content

rff_core/
dict.rs

1//! A small ordered string→string map for codec/format options — the role
2//! `AVDictionary` plays in FFmpeg (e.g. `-crf 23`, `-preset fast`). Ordered so
3//! that iteration (and any serialized form) is deterministic.
4
5use std::collections::BTreeMap;
6
7/// An ordered collection of `key = value` options.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct Dictionary {
10    entries: BTreeMap<String, String>,
11}
12
13impl Dictionary {
14    pub fn new() -> Dictionary {
15        Dictionary::default()
16    }
17
18    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
19        self.entries.insert(key.into(), value.into());
20    }
21
22    pub fn get(&self, key: &str) -> Option<&str> {
23        self.entries.get(key).map(String::as_str)
24    }
25
26    /// Parse an integer-valued option, returning `None` if absent or malformed.
27    pub fn get_int(&self, key: &str) -> Option<i64> {
28        self.get(key).and_then(|v| v.parse().ok())
29    }
30
31    /// Parse a bitrate-valued option with FFmpeg's `k`/`M` suffixes
32    /// (`128k` → 128 000, `1M` → 1 000 000; a bare number is taken as-is).
33    /// Case-insensitive; returns `None` if absent or malformed.
34    pub fn get_bitrate(&self, key: &str) -> Option<i64> {
35        let v = self.get(key)?.trim();
36        let (num, mult) = match v.chars().last() {
37            Some('k') | Some('K') => (&v[..v.len() - 1], 1_000i64),
38            Some('m') | Some('M') => (&v[..v.len() - 1], 1_000_000i64),
39            _ => (v, 1),
40        };
41        num.trim()
42            .parse::<f64>()
43            .ok()
44            .map(|n| (n * mult as f64) as i64)
45    }
46
47    pub fn is_empty(&self) -> bool {
48        self.entries.is_empty()
49    }
50
51    pub fn len(&self) -> usize {
52        self.entries.len()
53    }
54
55    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
56        self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::Dictionary;
63
64    #[test]
65    fn get_bitrate_parses_suffixes() {
66        let mut d = Dictionary::new();
67        d.set("b", "128k");
68        assert_eq!(d.get_bitrate("b"), Some(128_000));
69        d.set("b", "1M");
70        assert_eq!(d.get_bitrate("b"), Some(1_000_000));
71        d.set("b", "24000");
72        assert_eq!(d.get_bitrate("b"), Some(24_000));
73        d.set("b", "1.5M");
74        assert_eq!(d.get_bitrate("b"), Some(1_500_000));
75        d.set("b", "96K");
76        assert_eq!(d.get_bitrate("b"), Some(96_000));
77        assert_eq!(d.get_bitrate("missing"), None);
78        d.set("b", "garbage");
79        assert_eq!(d.get_bitrate("b"), None);
80    }
81}