Skip to main content

apple_plist/
error.rs

1//! The crate-wide error type and its [`Result`] alias.
2
3use crate::format::Format;
4
5/// A specialized [`Result`] for property-list operations.
6///
7/// [`Result`]: std::result::Result
8pub type Result<T, E = Error> = std::result::Result<T, E>;
9
10/// A boxed error carried as the optional cause of a parse failure.
11type Source = Box<dyn std::error::Error + Send + Sync>;
12
13/// Errors returned while encoding or decoding a property list.
14///
15/// The enum and its struct-like variants are `#[non_exhaustive]`, so new
16/// variants and fields are not breaking changes.
17#[derive(Debug, thiserror::Error)]
18#[non_exhaustive]
19pub enum Error {
20    /// An underlying reader or writer failed.
21    #[error("i/o error")]
22    Io(#[from] std::io::Error),
23
24    /// The input is not recognizable as a property list of `format`.
25    ///
26    /// This is the only variant that makes the decode ladder fall back from
27    /// the XML parser to the text parser.
28    #[error("invalid {format} property list")]
29    #[non_exhaustive]
30    InvalidPlist {
31        /// The parser that rejected the input: `"XML"`, `"binary"`, or `"text"`.
32        format: &'static str,
33        /// The underlying cause, when one exists.
34        source: Option<Source>,
35    },
36
37    /// The input was recognized as `format` but failed to parse. Never
38    /// triggers the XML-to-text fallback.
39    #[error("error parsing {format} property list")]
40    #[non_exhaustive]
41    Parse {
42        /// The parser that failed: `"XML"`, `"binary"`, or `"text"`.
43        format: &'static str,
44        /// The underlying cause, when one exists.
45        source: Option<Source>,
46    },
47
48    /// A value of this type cannot be represented in a property list.
49    #[error("can't marshal value of type {0}")]
50    UnknownType(&'static str),
51
52    /// A property-list value cannot decode into the requested target type.
53    #[error("cannot decode {found} into {expected}")]
54    #[non_exhaustive]
55    TypeMismatch {
56        /// A description of the requested target type.
57        expected: &'static str,
58        /// The property-list type name that was found.
59        found: &'static str,
60    },
61
62    /// Nesting exceeded [`MAX_PARSE_DEPTH`](crate::MAX_PARSE_DEPTH) while
63    /// parsing — the input is too deeply nested to process safely.
64    #[error("maximum nesting depth exceeded")]
65    MaxDepthExceeded,
66
67    /// A scalar literal failed to parse (integer, real, boolean, or date).
68    #[error("{0}")]
69    ParseScalar(String),
70
71    /// Encoding produced no root element to write.
72    #[error("no root element to encode")]
73    NoRootElement,
74
75    /// A null value reached a position where property lists cannot express it.
76    #[error("null is not representable")]
77    NullNotRepresentable,
78
79    /// A free-form message, used by `serde` `Error::custom` and similar.
80    #[error("{0}")]
81    Message(String),
82
83    /// The requested output format is behind a cargo feature that is not
84    /// enabled in this build.
85    #[error("support for the {format} format is disabled")]
86    #[non_exhaustive]
87    FeatureDisabled {
88        /// The format whose codec is compiled out.
89        format: Format,
90    },
91}
92
93impl Error {
94    /// Compiled only where a decode-ladder rung is feature-disabled; the
95    /// enabled rungs build their retry signals with sources attached.
96    #[cfg(any(test, not(feature = "binary"), not(feature = "xml")))]
97    pub(crate) fn invalid(format: &'static str) -> Self {
98        Self::InvalidPlist {
99            format,
100            source: None,
101        }
102    }
103
104    #[cfg(any(test, feature = "binary", feature = "xml", feature = "openstep"))]
105    pub(crate) fn parse(format: &'static str, source: impl Into<Source>) -> Self {
106        Self::Parse {
107            format,
108            source: Some(source.into()),
109        }
110    }
111
112    /// True only for [`Error::InvalidPlist`] — the decode ladder's signal to
113    /// retry the buffered input with the text parser.
114    pub(crate) const fn is_retry_signal(&self) -> bool {
115        matches!(self, Self::InvalidPlist { .. })
116    }
117}
118
119#[cfg(feature = "serde")]
120impl serde::ser::Error for Error {
121    fn custom<T: std::fmt::Display>(msg: T) -> Self {
122        Self::Message(msg.to_string())
123    }
124}
125
126#[cfg(feature = "serde")]
127impl serde::de::Error for Error {
128    fn custom<T: std::fmt::Display>(msg: T) -> Self {
129        Self::Message(msg.to_string())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    const fn assert_send_sync<T: Send + Sync + 'static>() {}
138    const _: () = assert_send_sync::<Error>();
139
140    #[test]
141    fn display_messages_are_lowercase_unprefixed_unterminated() {
142        let cases = [
143            (Error::invalid("XML"), "invalid XML property list"),
144            (
145                Error::parse("binary", "bad trailer"),
146                "error parsing binary property list",
147            ),
148            (
149                Error::UnknownType("chan"),
150                "can't marshal value of type chan",
151            ),
152            (
153                Error::TypeMismatch {
154                    expected: "u64",
155                    found: "string",
156                },
157                "cannot decode string into u64",
158            ),
159            (Error::MaxDepthExceeded, "maximum nesting depth exceeded"),
160            (
161                Error::ParseScalar("invalid digit found in string".to_owned()),
162                "invalid digit found in string",
163            ),
164            (Error::NoRootElement, "no root element to encode"),
165            (Error::NullNotRepresentable, "null is not representable"),
166            (Error::Message("boom".to_owned()), "boom"),
167            (
168                Error::FeatureDisabled {
169                    format: Format::Binary,
170                },
171                "support for the Binary format is disabled",
172            ),
173        ];
174        for (err, want) in cases {
175            assert_eq!(err.to_string(), want);
176        }
177    }
178
179    #[test]
180    fn parse_carries_its_source() {
181        let err = Error::parse("text", "unterminated string");
182        let source = std::error::Error::source(&err).map(ToString::to_string);
183        assert_eq!(source.as_deref(), Some("unterminated string"));
184    }
185
186    #[test]
187    fn invalid_has_no_source() {
188        let err = Error::invalid("XML");
189        assert!(std::error::Error::source(&err).is_none());
190    }
191
192    #[test]
193    fn retry_signal_is_invalid_plist_only() {
194        assert!(Error::invalid("XML").is_retry_signal());
195        assert!(!Error::parse("XML", "boom").is_retry_signal());
196        assert!(!Error::MaxDepthExceeded.is_retry_signal());
197        assert!(!Error::Io(std::io::Error::other("io")).is_retry_signal());
198    }
199
200    #[cfg(feature = "serde")]
201    #[test]
202    fn serde_custom_maps_to_message() {
203        let ser = <Error as serde::ser::Error>::custom("ser oops");
204        let de = <Error as serde::de::Error>::custom("de oops");
205        assert!(matches!(ser, Error::Message(ref m) if m == "ser oops"));
206        assert!(matches!(de, Error::Message(ref m) if m == "de oops"));
207    }
208}