Skip to main content

apple_plist/value/
mod.rs

1//! The owned property-list value tree.
2
3mod integer;
4mod real;
5
6#[cfg(feature = "serde")]
7pub(crate) mod de;
8#[cfg(feature = "serde")]
9pub(crate) mod ser;
10
11use indexmap::IndexMap;
12
13pub use self::integer::Integer;
14pub use self::real::Real;
15use crate::date::Date;
16#[cfg(any(test, feature = "xml", feature = "openstep"))]
17use crate::scalar;
18use crate::uid::Uid;
19
20/// An order-preserving plist dictionary.
21///
22/// Keys iterate in insertion order, matching how Apple property lists round-trip
23/// dictionary keys. Backed by [`IndexMap`], so it carries the full map surface
24/// (`get`, `insert`, `entry`, `keys`, `values`, `sort_keys`, indexing).
25pub type Dictionary = IndexMap<String, Value>;
26
27/// Any value a property list can hold.
28///
29/// The tree is owned (`String` / `Vec` / [`Dictionary`], no lifetimes) and
30/// covers the nine property-list value kinds. Dictionaries preserve key
31/// insertion order.
32///
33/// The enum is `#[non_exhaustive]`; equality is `PartialEq` only, because a
34/// real may hold `NaN`.
35///
36/// # Examples
37///
38/// ```
39/// use apple_plist::Value;
40///
41/// let value = Value::from_iter([
42///     ("name".to_owned(), Value::from("plist")),
43///     ("count".to_owned(), Value::from(3u8)),
44/// ]);
45/// let dict = value.as_dictionary().expect("built a dictionary");
46/// assert_eq!(dict.get("name").and_then(Value::as_str), Some("plist"));
47/// ```
48#[derive(Clone, Debug, PartialEq)]
49#[non_exhaustive]
50pub enum Value {
51    /// A dictionary; keys preserve insertion order.
52    Dictionary(Dictionary),
53    /// An ordered array.
54    Array(Vec<Self>),
55    /// A UTF-8 string.
56    String(String),
57    /// An integer, signed or unsigned.
58    Integer(Integer),
59    /// A floating-point number.
60    Real(Real),
61    /// A boolean.
62    Boolean(bool),
63    /// A keyed-archive `CF$UID` reference.
64    Uid(Uid),
65    /// Raw bytes.
66    Data(Vec<u8>),
67    /// An absolute point in time.
68    Date(Date),
69}
70
71impl Value {
72    /// The kind name for this variant, used verbatim in decode-type-mismatch
73    /// error messages.
74    #[cfg(any(test, feature = "serde"))]
75    pub(crate) const fn type_name(&self) -> &'static str {
76        match self {
77            Self::Dictionary(_) => "dictionary",
78            Self::Array(_) => "array",
79            Self::String(_) => "string",
80            Self::Integer(_) => "integer",
81            Self::Real(_) => "real",
82            Self::Boolean(_) => "boolean",
83            Self::Uid(_) => "UID",
84            Self::Data(_) => "data",
85            Self::Date(_) => "date",
86        }
87    }
88
89    /// Returns the dictionary behind [`Value::Dictionary`].
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use apple_plist::{Dictionary, Value};
95    ///
96    /// let value = Value::from(Dictionary::new());
97    /// assert!(value.as_dictionary().is_some());
98    /// assert!(Value::from(true).as_dictionary().is_none());
99    /// ```
100    #[must_use]
101    pub const fn as_dictionary(&self) -> Option<&Dictionary> {
102        match self {
103            Self::Dictionary(dict) => Some(dict),
104            _ => None,
105        }
106    }
107
108    /// Returns the dictionary behind [`Value::Dictionary`], mutably.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use apple_plist::{Dictionary, Value};
114    ///
115    /// let mut value = Value::from(Dictionary::new());
116    /// if let Some(dict) = value.as_dictionary_mut() {
117    ///     let _ = dict.insert("key".to_owned(), Value::from(1u8));
118    /// }
119    /// assert_eq!(value.as_dictionary().map(Dictionary::len), Some(1));
120    /// ```
121    #[must_use]
122    pub const fn as_dictionary_mut(&mut self) -> Option<&mut Dictionary> {
123        match self {
124            Self::Dictionary(dict) => Some(dict),
125            _ => None,
126        }
127    }
128
129    /// Returns the elements behind [`Value::Array`].
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use apple_plist::Value;
135    ///
136    /// let value = Value::from(vec![Value::from(true)]);
137    /// assert_eq!(value.as_array().map(Vec::len), Some(1));
138    /// assert!(Value::from(true).as_array().is_none());
139    /// ```
140    #[must_use]
141    pub const fn as_array(&self) -> Option<&Vec<Self>> {
142        match self {
143            Self::Array(values) => Some(values),
144            _ => None,
145        }
146    }
147
148    /// Returns the elements behind [`Value::Array`], mutably.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use apple_plist::Value;
154    ///
155    /// let mut value = Value::from(Vec::<Value>::new());
156    /// if let Some(values) = value.as_array_mut() {
157    ///     values.push(Value::from(7i32));
158    /// }
159    /// assert_eq!(value.as_array().map(Vec::len), Some(1));
160    /// ```
161    #[must_use]
162    pub const fn as_array_mut(&mut self) -> Option<&mut Vec<Self>> {
163        match self {
164            Self::Array(values) => Some(values),
165            _ => None,
166        }
167    }
168
169    /// Returns the string slice behind [`Value::String`].
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use apple_plist::Value;
175    ///
176    /// assert_eq!(Value::from("hello").as_str(), Some("hello"));
177    /// assert_eq!(Value::from(false).as_str(), None);
178    /// ```
179    #[must_use]
180    pub const fn as_str(&self) -> Option<&str> {
181        match self {
182            Self::String(s) => Some(s.as_str()),
183            _ => None,
184        }
185    }
186
187    /// Returns the integer behind [`Value::Integer`].
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use apple_plist::{Integer, Value};
193    ///
194    /// assert_eq!(Value::from(-1i64).as_integer(), Some(Integer::Signed(-1)));
195    /// assert_eq!(Value::from("1").as_integer(), None);
196    /// ```
197    #[must_use]
198    pub const fn as_integer(&self) -> Option<Integer> {
199        match self {
200            Self::Integer(integer) => Some(*integer),
201            _ => None,
202        }
203    }
204
205    /// Returns the numeric value behind [`Value::Real`] as an `f64`.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use apple_plist::Value;
211    ///
212    /// assert_eq!(Value::from(1.5).as_real(), Some(1.5));
213    /// assert_eq!(Value::from(1u8).as_real(), None);
214    /// ```
215    #[must_use]
216    pub const fn as_real(&self) -> Option<f64> {
217        match self {
218            Self::Real(real) => Some(real.value()),
219            _ => None,
220        }
221    }
222
223    /// Returns the boolean behind [`Value::Boolean`].
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use apple_plist::Value;
229    ///
230    /// assert_eq!(Value::from(true).as_boolean(), Some(true));
231    /// assert_eq!(Value::from("true").as_boolean(), None);
232    /// ```
233    #[must_use]
234    pub const fn as_boolean(&self) -> Option<bool> {
235        match self {
236            Self::Boolean(b) => Some(*b),
237            _ => None,
238        }
239    }
240
241    /// Returns the bytes behind [`Value::Data`].
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use apple_plist::Value;
247    ///
248    /// assert_eq!(Value::from(vec![1u8, 2]).as_data(), Some(&[1u8, 2][..]));
249    /// assert_eq!(Value::from("12").as_data(), None);
250    /// ```
251    #[must_use]
252    pub const fn as_data(&self) -> Option<&[u8]> {
253        match self {
254            Self::Data(data) => Some(data.as_slice()),
255            _ => None,
256        }
257    }
258
259    /// Returns the date behind [`Value::Date`].
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use std::time::SystemTime;
265    ///
266    /// use apple_plist::{Date, Value};
267    ///
268    /// let date = Date::from(SystemTime::UNIX_EPOCH);
269    /// assert_eq!(Value::from(date).as_date(), Some(date));
270    /// assert_eq!(Value::from(0i64).as_date(), None);
271    /// ```
272    #[must_use]
273    pub const fn as_date(&self) -> Option<Date> {
274        match self {
275            Self::Date(date) => Some(*date),
276            _ => None,
277        }
278    }
279
280    /// Returns the UID behind [`Value::Uid`].
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// use apple_plist::{Uid, Value};
286    ///
287    /// assert_eq!(Value::from(Uid::from(7)).as_uid(), Some(Uid::from(7)));
288    /// assert_eq!(Value::from(7u64).as_uid(), None);
289    /// ```
290    #[must_use]
291    pub const fn as_uid(&self) -> Option<Uid> {
292        match self {
293            Self::Uid(uid) => Some(*uid),
294            _ => None,
295        }
296    }
297
298    /// Consumes the value, returning the dictionary behind
299    /// [`Value::Dictionary`].
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use apple_plist::{Dictionary, Value};
305    ///
306    /// assert_eq!(
307    ///     Value::from(Dictionary::new()).into_dictionary(),
308    ///     Some(Dictionary::new())
309    /// );
310    /// assert_eq!(Value::from(true).into_dictionary(), None);
311    /// ```
312    #[must_use]
313    pub fn into_dictionary(self) -> Option<Dictionary> {
314        match self {
315            Self::Dictionary(dict) => Some(dict),
316            _ => None,
317        }
318    }
319
320    /// Consumes the value, returning the elements behind [`Value::Array`].
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use apple_plist::Value;
326    ///
327    /// assert_eq!(
328    ///     Value::from(vec![Value::from(1u8)]).into_array(),
329    ///     Some(vec![Value::from(1u8)])
330    /// );
331    /// assert_eq!(Value::from(1u8).into_array(), None);
332    /// ```
333    #[must_use]
334    pub fn into_array(self) -> Option<Vec<Self>> {
335        match self {
336            Self::Array(values) => Some(values),
337            _ => None,
338        }
339    }
340
341    /// Consumes the value, returning the string behind [`Value::String`].
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use apple_plist::Value;
347    ///
348    /// assert_eq!(Value::from("hi").into_string(), Some("hi".to_owned()));
349    /// assert_eq!(Value::from(true).into_string(), None);
350    /// ```
351    #[must_use]
352    pub fn into_string(self) -> Option<String> {
353        match self {
354            Self::String(s) => Some(s),
355            _ => None,
356        }
357    }
358
359    /// Consumes the value, returning the bytes behind [`Value::Data`].
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// use apple_plist::Value;
365    ///
366    /// assert_eq!(Value::from(vec![1u8, 2]).into_data(), Some(vec![1u8, 2]));
367    /// assert_eq!(Value::from("12").into_data(), None);
368    /// ```
369    #[must_use]
370    pub fn into_data(self) -> Option<Vec<u8>> {
371        match self {
372            Self::Data(data) => Some(data),
373            _ => None,
374        }
375    }
376}
377
378/// Applies the `CF$UID` rewrite to a just-parsed dictionary's raw entry list,
379/// before duplicate keys collapse into the map.
380///
381/// Converts iff there is exactly one pair, its key is byte-exactly `CF$UID`,
382/// and its value is an integer (signedness ignored: the raw bits become the
383/// UID) — or, when `lax` (OpenStep), a string parsable as a base-10 `u64`.
384/// Anything else collapses to a dictionary, last duplicate key winning; there
385/// is no error path.
386#[cfg(any(test, feature = "xml", feature = "openstep"))]
387pub(crate) fn maybe_uid(entries: Vec<(String, Value)>, lax: bool) -> Value {
388    if let [(key, value)] = entries.as_slice()
389        && key == "CF$UID"
390    {
391        match value {
392            Value::Integer(integer) => return Value::Uid(Uid::from(integer.to_raw_parts().1)),
393            Value::String(s) if lax => {
394                if let Ok(uid) = scalar::parse_u64(s, 10) {
395                    return Value::Uid(Uid::from(uid));
396                }
397            }
398            _ => {}
399        }
400    }
401    Value::Dictionary(entries.into_iter().collect())
402}
403
404impl From<&str> for Value {
405    fn from(value: &str) -> Self {
406        Self::String(value.to_owned())
407    }
408}
409
410impl From<String> for Value {
411    fn from(value: String) -> Self {
412        Self::String(value)
413    }
414}
415
416impl From<bool> for Value {
417    fn from(value: bool) -> Self {
418        Self::Boolean(value)
419    }
420}
421
422impl From<f64> for Value {
423    fn from(value: f64) -> Self {
424        Self::Real(Real::from(value))
425    }
426}
427
428impl From<Real> for Value {
429    fn from(value: Real) -> Self {
430        Self::Real(value)
431    }
432}
433
434impl From<Integer> for Value {
435    fn from(value: Integer) -> Self {
436        Self::Integer(value)
437    }
438}
439
440impl From<Uid> for Value {
441    fn from(value: Uid) -> Self {
442        Self::Uid(value)
443    }
444}
445
446impl From<Date> for Value {
447    fn from(value: Date) -> Self {
448        Self::Date(value)
449    }
450}
451
452impl From<Vec<Self>> for Value {
453    fn from(value: Vec<Self>) -> Self {
454        Self::Array(value)
455    }
456}
457
458impl From<Dictionary> for Value {
459    fn from(value: Dictionary) -> Self {
460        Self::Dictionary(value)
461    }
462}
463
464/// The `[]byte → cfData` rule at the literal level: a byte vector is data,
465/// never an array. Element-wise arrays go through `FromIterator`.
466impl From<Vec<u8>> for Value {
467    fn from(value: Vec<u8>) -> Self {
468        Self::Data(value)
469    }
470}
471
472macro_rules! impl_from_int {
473    ($($ty:ty),+) => {$(
474        impl From<$ty> for Value {
475            fn from(value: $ty) -> Self {
476                Self::Integer(Integer::from(value))
477            }
478        }
479    )+};
480}
481
482impl_from_int!(i8, i16, i32, i64, u8, u16, u32, u64);
483
484impl FromIterator<Self> for Value {
485    fn from_iter<T: IntoIterator<Item = Self>>(iter: T) -> Self {
486        Self::Array(iter.into_iter().collect())
487    }
488}
489
490/// Builds a dictionary in iteration order; the last value wins for duplicate
491/// keys, and the key keeps its first position.
492impl FromIterator<(String, Self)> for Value {
493    fn from_iter<T: IntoIterator<Item = (String, Self)>>(iter: T) -> Self {
494        Self::Dictionary(iter.into_iter().collect())
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    fn entry(key: &str, value: Value) -> (String, Value) {
503        (key.to_owned(), value)
504    }
505
506    #[test]
507    fn type_names_are_verbatim() {
508        let cases = [
509            (Value::Dictionary(Dictionary::new()), "dictionary"),
510            (Value::Array(Vec::new()), "array"),
511            (Value::from(""), "string"),
512            (Value::from(0u8), "integer"),
513            (Value::from(0.0), "real"),
514            (Value::from(false), "boolean"),
515            (Value::from(Uid::from(0)), "UID"),
516            (Value::from(Vec::<u8>::new()), "data"),
517            (
518                Value::from(Date::from(std::time::SystemTime::UNIX_EPOCH)),
519                "date",
520            ),
521        ];
522        for (value, want) in cases {
523            assert_eq!(value.type_name(), want);
524        }
525    }
526
527    #[test]
528    fn accessors_are_some_only_on_the_matching_variant() {
529        let string = Value::from("s");
530        assert_eq!(string.as_str(), Some("s"));
531        assert!(string.as_integer().is_none());
532        assert!(string.as_real().is_none());
533        assert!(string.as_boolean().is_none());
534        assert!(string.as_data().is_none());
535        assert!(string.as_date().is_none());
536        assert!(string.as_uid().is_none());
537        assert!(string.as_dictionary().is_none());
538        assert!(string.as_array().is_none());
539
540        // No coercion: an Integer is not a Real and bytes are not a string.
541        assert!(Value::from(5i64).as_real().is_none());
542        assert!(Value::from(vec![0x68u8]).as_str().is_none());
543    }
544
545    #[test]
546    fn mutable_and_consuming_accessors_work() {
547        let mut value = Value::from(Dictionary::new());
548        if let Some(dict) = value.as_dictionary_mut() {
549            drop(dict.insert("k".to_owned(), Value::from(1u8)));
550        }
551        assert_eq!(value.clone().into_dictionary().map(|d| d.len()), Some(1));
552        assert!(value.into_array().is_none());
553
554        let mut array = Value::from(vec![Value::from(1u8)]);
555        if let Some(values) = array.as_array_mut() {
556            values.push(Value::from(2u8));
557        }
558        assert_eq!(array.into_array().map(|v| v.len()), Some(2));
559
560        assert_eq!(Value::from("hi").into_string(), Some("hi".to_owned()));
561        assert_eq!(Value::from(vec![1u8]).into_data(), Some(vec![1u8]));
562        assert!(Value::from(1u8).into_string().is_none());
563        assert!(Value::from("x").into_data().is_none());
564    }
565
566    #[test]
567    fn byte_vectors_become_data_not_arrays() {
568        assert_eq!(Value::from(vec![104u8, 105]).type_name(), "data");
569        let array: Value = [Value::from(104u8), Value::from(105u8)]
570            .into_iter()
571            .collect();
572        assert_eq!(array.type_name(), "array");
573    }
574
575    #[test]
576    fn from_f64_is_wide_and_from_real_preserves_width() {
577        assert_eq!(Value::from(1.5).as_real(), Some(1.5));
578        let narrow = Value::from(Real::from(1.5f32));
579        assert_eq!(narrow.as_real(), Some(1.5));
580    }
581
582    #[test]
583    fn dictionary_from_iterator_keeps_the_last_duplicate_key() {
584        let value: Value = [
585            entry("dup", Value::from(1u8)),
586            entry("other", Value::from(2u8)),
587            entry("dup", Value::from(3u8)),
588        ]
589        .into_iter()
590        .collect();
591        let dict = value.as_dictionary().cloned().unwrap_or_default();
592        assert_eq!(dict.get("dup"), Some(&Value::from(3u8)));
593        assert_eq!(dict.len(), 2);
594    }
595
596    #[test]
597    fn dictionary_iterates_in_insertion_order() {
598        let value: Value = [
599            entry("zebra", Value::from(1u8)),
600            entry("Alpha", Value::from(2u8)),
601            entry("alpha", Value::from(3u8)),
602        ]
603        .into_iter()
604        .collect();
605        let keys: Vec<&str> = value
606            .as_dictionary()
607            .into_iter()
608            .flatten()
609            .map(|(k, _)| k.as_str())
610            .collect();
611        assert_eq!(keys, ["zebra", "Alpha", "alpha"]);
612    }
613
614    #[test]
615    fn maybe_uid_collapses_the_single_integer_pair() {
616        let converted = maybe_uid(vec![entry("CF$UID", Value::from(255u8))], false);
617        assert_eq!(converted, Value::Uid(Uid::from(255)));
618    }
619
620    #[test]
621    fn maybe_uid_ignores_signedness() {
622        let converted = maybe_uid(vec![entry("CF$UID", Value::from(-1i64))], false);
623        assert_eq!(converted, Value::Uid(Uid::from(u64::MAX)));
624    }
625
626    #[test]
627    fn maybe_uid_accepts_numeric_strings_only_when_lax() {
628        let lax = maybe_uid(vec![entry("CF$UID", Value::from("12"))], true);
629        assert_eq!(lax.as_uid(), Some(Uid::from(12)));
630
631        let strict = maybe_uid(vec![entry("CF$UID", Value::from("12"))], false);
632        assert!(strict.as_dictionary().is_some());
633    }
634
635    #[test]
636    fn maybe_uid_lax_strings_parse_as_u64() {
637        for bad in ["+5", "-1", "0x10", "5_0", "", "18446744073709551616", " 5"] {
638            let value = maybe_uid(vec![entry("CF$UID", Value::from(bad))], true);
639            assert!(value.as_dictionary().is_some(), "{bad}");
640        }
641        let max = maybe_uid(
642            vec![entry("CF$UID", Value::from("18446744073709551615"))],
643            true,
644        );
645        assert_eq!(max.as_uid(), Some(Uid::from(u64::MAX)));
646    }
647
648    #[test]
649    fn maybe_uid_leaves_other_shapes_as_dictionaries() {
650        // Two raw entries — even both named CF$UID — stay a dictionary.
651        let duplicate = maybe_uid(
652            vec![
653                entry("CF$UID", Value::from(1u8)),
654                entry("CF$UID", Value::from(2u8)),
655            ],
656            true,
657        );
658        let dict = duplicate.as_dictionary().cloned().unwrap_or_default();
659        assert_eq!(dict.len(), 1);
660        assert_eq!(dict.get("CF$UID"), Some(&Value::from(2u8)));
661
662        for value in [
663            maybe_uid(vec![entry("cf$uid", Value::from(1u8))], true),
664            maybe_uid(vec![entry("CF$UID ", Value::from(1u8))], true),
665            maybe_uid(vec![entry("CF$UID", Value::from(1.0))], true),
666            maybe_uid(vec![entry("CF$UID", Value::from(true))], true),
667            maybe_uid(Vec::new(), true),
668            maybe_uid(
669                vec![
670                    entry("CF$UID", Value::from(1u8)),
671                    entry("extra", Value::from(2u8)),
672                ],
673                true,
674            ),
675        ] {
676            assert!(value.as_dictionary().is_some());
677        }
678    }
679
680    #[test]
681    fn value_equality_follows_payload_semantics() {
682        assert_eq!(Value::from(5i64), Value::from(5u64));
683        assert_ne!(Value::from(f64::NAN), Value::from(f64::NAN));
684        assert_eq!(Value::from(Real::from(2.0f32)), Value::from(2.0f64));
685    }
686}