Skip to main content

apple_plist/value/
integer.rs

1//! The dual signed/unsigned 64-bit integer model.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6
7/// A property-list integer, carrying its signedness.
8///
9/// Plists must represent both `u64::MAX` and negative values, so the model is
10/// the pair of a raw 64-bit payload and a sign flag rather than a single
11/// `i64`. Equality, ordering, and hashing are **numeric**: no codec preserves
12/// signedness for non-negative values, so `Signed(5)` and `Unsigned(5)` are
13/// the same integer (and hash identically).
14///
15/// # Examples
16///
17/// ```
18/// use apple_plist::Integer;
19///
20/// assert_eq!(Integer::from(5i64), Integer::from(5u64));
21/// assert!(Integer::from(-1i8) < Integer::from(0u8));
22/// ```
23#[derive(Clone, Copy, Debug)]
24pub enum Integer {
25    /// A signed value; the only representation negative integers have.
26    Signed(i64),
27    /// An unsigned value, covering `i64::MAX + 1 ..= u64::MAX` exclusively.
28    Unsigned(u64),
29}
30
31impl Integer {
32    /// Returns the value as an `i64` when it fits.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use apple_plist::Integer;
38    ///
39    /// assert_eq!(
40    ///     Integer::Unsigned(i64::MAX as u64).as_signed(),
41    ///     Some(i64::MAX)
42    /// );
43    /// assert_eq!(Integer::Unsigned(i64::MAX as u64 + 1).as_signed(), None);
44    /// assert_eq!(Integer::Signed(-1).as_signed(), Some(-1));
45    /// ```
46    #[must_use]
47    pub const fn as_signed(self) -> Option<i64> {
48        match self {
49            Self::Signed(value) => Some(value),
50            Self::Unsigned(value) => {
51                if value <= i64::MAX.cast_unsigned() {
52                    Some(value.cast_signed())
53                } else {
54                    None
55                }
56            }
57        }
58    }
59
60    /// Returns the value as a `u64` when it is non-negative.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use apple_plist::Integer;
66    ///
67    /// assert_eq!(Integer::Signed(0).as_unsigned(), Some(0));
68    /// assert_eq!(Integer::Signed(-1).as_unsigned(), None);
69    /// assert_eq!(Integer::Unsigned(u64::MAX).as_unsigned(), Some(u64::MAX));
70    /// ```
71    #[must_use]
72    pub const fn as_unsigned(self) -> Option<u64> {
73        match self {
74            Self::Unsigned(value) => Some(value),
75            Self::Signed(value) => {
76                if value >= 0 {
77                    Some(value.cast_unsigned())
78                } else {
79                    None
80                }
81            }
82        }
83    }
84
85    /// The structural `(signed, raw bits)` pair — the binary encoder's dedup
86    /// key and the `CF$UID` payload, where signedness is ignored.
87    #[cfg(any(test, feature = "binary", feature = "xml", feature = "openstep"))]
88    pub(crate) const fn to_raw_parts(self) -> (bool, u64) {
89        match self {
90            Self::Signed(value) => (true, value.cast_unsigned()),
91            Self::Unsigned(value) => (false, value),
92        }
93    }
94
95    fn widen(self) -> i128 {
96        match self {
97            Self::Signed(value) => i128::from(value),
98            Self::Unsigned(value) => i128::from(value),
99        }
100    }
101}
102
103impl PartialEq for Integer {
104    fn eq(&self, other: &Self) -> bool {
105        match (*self, *other) {
106            (Self::Signed(a), Self::Signed(b)) => a == b,
107            (Self::Unsigned(a), Self::Unsigned(b)) => a == b,
108            (Self::Signed(s), Self::Unsigned(u)) | (Self::Unsigned(u), Self::Signed(s)) => {
109                s >= 0 && s.cast_unsigned() == u
110            }
111        }
112    }
113}
114
115impl Eq for Integer {}
116
117impl Hash for Integer {
118    fn hash<H: Hasher>(&self, state: &mut H) {
119        // One canonical input per numeric value: non-negative values hash as
120        // u64 regardless of variant; negatives exist only as Signed.
121        match *self {
122            Self::Unsigned(value) => state.write_u64(value),
123            Self::Signed(value) if value >= 0 => state.write_u64(value.cast_unsigned()),
124            Self::Signed(value) => state.write_i64(value),
125        }
126    }
127}
128
129impl Ord for Integer {
130    fn cmp(&self, other: &Self) -> Ordering {
131        self.widen().cmp(&other.widen())
132    }
133}
134
135impl PartialOrd for Integer {
136    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
137        Some(self.cmp(other))
138    }
139}
140
141impl fmt::Display for Integer {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Self::Signed(value) => fmt::Display::fmt(value, f),
145            Self::Unsigned(value) => fmt::Display::fmt(value, f),
146        }
147    }
148}
149
150macro_rules! impl_from_signed {
151    ($($ty:ty),+) => {$(
152        impl From<$ty> for Integer {
153            fn from(value: $ty) -> Self {
154                Self::Signed(i64::from(value))
155            }
156        }
157    )+};
158}
159
160macro_rules! impl_from_unsigned {
161    ($($ty:ty),+) => {$(
162        impl From<$ty> for Integer {
163            fn from(value: $ty) -> Self {
164                Self::Unsigned(u64::from(value))
165            }
166        }
167    )+};
168}
169
170impl_from_signed!(i8, i16, i32, i64);
171impl_from_unsigned!(u8, u16, u32, u64);
172
173#[cfg(test)]
174mod tests {
175    use std::hash::{BuildHasher, RandomState};
176
177    use super::*;
178
179    fn hashes_equal(a: Integer, b: Integer) -> bool {
180        let state = RandomState::new();
181        state.hash_one(a) == state.hash_one(b)
182    }
183
184    #[test]
185    fn equality_is_numeric_across_variants() {
186        assert_eq!(Integer::Signed(5), Integer::Unsigned(5));
187        assert_eq!(Integer::Unsigned(5), Integer::Signed(5));
188        assert_eq!(Integer::Signed(0), Integer::Unsigned(0));
189        assert_ne!(Integer::Signed(-1), Integer::Unsigned(u64::MAX));
190        assert_ne!(
191            Integer::Signed(i64::MIN),
192            Integer::Unsigned(i64::MIN.unsigned_abs())
193        );
194        assert_eq!(
195            Integer::Signed(i64::MAX),
196            Integer::Unsigned(i64::MAX.cast_unsigned())
197        );
198    }
199
200    #[test]
201    fn equal_values_hash_equally() {
202        let pairs = [
203            (Integer::Signed(5), Integer::Unsigned(5)),
204            (Integer::Signed(0), Integer::Unsigned(0)),
205            (
206                Integer::Signed(i64::MAX),
207                Integer::Unsigned(i64::MAX.cast_unsigned()),
208            ),
209        ];
210        for (a, b) in pairs {
211            assert_eq!(a, b);
212            assert!(hashes_equal(a, b));
213        }
214        // Benign documented collision: write_i64(v) == write_u64(v as u64).
215        assert!(hashes_equal(
216            Integer::Signed(-1),
217            Integer::Unsigned(u64::MAX)
218        ));
219        assert_ne!(Integer::Signed(-1), Integer::Unsigned(u64::MAX));
220    }
221
222    #[test]
223    fn ordering_is_total_numeric_order() {
224        let mut values = [
225            Integer::Unsigned(u64::MAX),
226            Integer::Signed(-1),
227            Integer::Unsigned(0),
228            Integer::Signed(i64::MIN),
229            Integer::Unsigned(i64::MAX.cast_unsigned() + 1),
230            Integer::Signed(i64::MAX),
231        ];
232        values.sort_unstable();
233        assert_eq!(
234            values,
235            [
236                Integer::Signed(i64::MIN),
237                Integer::Signed(-1),
238                Integer::Unsigned(0),
239                Integer::Signed(i64::MAX),
240                Integer::Unsigned(i64::MAX.cast_unsigned() + 1),
241                Integer::Unsigned(u64::MAX),
242            ]
243        );
244        assert!(Integer::Signed(-1) < Integer::Unsigned(0));
245        assert!(Integer::Signed(i64::MAX) < Integer::Unsigned(i64::MAX.cast_unsigned() + 1));
246        assert_eq!(
247            Integer::Signed(7).cmp(&Integer::Unsigned(7)),
248            Ordering::Equal
249        );
250    }
251
252    #[test]
253    fn accessor_boundaries_match_the_spec() {
254        assert_eq!(
255            Integer::Unsigned(i64::MAX.cast_unsigned()).as_signed(),
256            Some(i64::MAX)
257        );
258        assert_eq!(
259            Integer::Unsigned(i64::MAX.cast_unsigned() + 1).as_signed(),
260            None
261        );
262        assert_eq!(Integer::Signed(-1).as_unsigned(), None);
263        assert_eq!(Integer::Signed(0).as_unsigned(), Some(0));
264        assert_eq!(Integer::Signed(i64::MIN).as_signed(), Some(i64::MIN));
265    }
266
267    #[test]
268    fn raw_parts_expose_signedness_and_bits() {
269        assert_eq!(Integer::Signed(-1).to_raw_parts(), (true, u64::MAX));
270        assert_eq!(Integer::Signed(5).to_raw_parts(), (true, 5));
271        assert_eq!(Integer::Unsigned(5).to_raw_parts(), (false, 5));
272        assert_eq!(
273            Integer::Signed(i64::MIN).to_raw_parts(),
274            (true, 0x8000_0000_0000_0000)
275        );
276    }
277
278    #[test]
279    fn display_prints_canonical_digits() {
280        assert_eq!(Integer::Signed(5).to_string(), "5");
281        assert_eq!(Integer::Unsigned(5).to_string(), "5");
282        assert_eq!(
283            Integer::Signed(-9_223_372_036_854_775_808).to_string(),
284            "-9223372036854775808"
285        );
286        assert_eq!(
287            Integer::Unsigned(u64::MAX).to_string(),
288            "18446744073709551615"
289        );
290    }
291
292    #[test]
293    fn from_impls_cover_all_eight_fixed_width_ints() {
294        assert_eq!(Integer::from(-1i8), Integer::Signed(-1));
295        assert_eq!(Integer::from(-1i16), Integer::Signed(-1));
296        assert_eq!(Integer::from(-1i32), Integer::Signed(-1));
297        assert_eq!(Integer::from(-1i64), Integer::Signed(-1));
298        assert_eq!(Integer::from(1u8), Integer::Unsigned(1));
299        assert_eq!(Integer::from(1u16), Integer::Unsigned(1));
300        assert_eq!(Integer::from(1u32), Integer::Unsigned(1));
301        assert_eq!(Integer::from(u64::MAX), Integer::Unsigned(u64::MAX));
302    }
303}