Skip to main content

ipp_printer_app/
flags.rs

1// The flag constants below have self-descriptive names (`COVER_OPEN`,
2// `MEDIA_JAM`, …). `bitflags!` expands the constants into a separate impl
3// block that attributes attached to the struct don't reach, so we allow at
4// module scope rather than walking 17 consts.
5#![allow(missing_docs)]
6
7//! IPP `printer-state-reasons` bit flags (PWG 5101.1 keywords).
8
9use bitflags::bitflags;
10
11/// Underlying integer representation for [`PrinterReason`].
12pub type PrinterReasonRaw = u32;
13
14bitflags! {
15    /// `printer-state-reasons` flags. Use [`PrinterReason::empty`] for
16    /// "no reasons"; do NOT define a `NONE = 0` constant — bitflags
17    /// `.contains(zero)` is always `true`, making it a footgun.
18    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19    #[allow(missing_docs)]
20    pub struct PrinterReason: PrinterReasonRaw {
21        const OTHER = 0x0001;
22        const COVER_OPEN = 0x0002;
23        const INPUT_TRAY_MISSING = 0x0004;
24        const MARKER_SUPPLY_EMPTY = 0x0008;
25        const MARKER_SUPPLY_LOW = 0x0010;
26        const MARKER_WASTE_ALMOST_FULL = 0x0020;
27        const MARKER_WASTE_FULL = 0x0040;
28        const MEDIA_EMPTY = 0x0080;
29        const MEDIA_JAM = 0x0100;
30        const MEDIA_LOW = 0x0200;
31        const MEDIA_NEEDED = 0x0400;
32        const OFFLINE = 0x0800;
33        const SPOOL_AREA_FULL = 0x1000;
34        const TONER_EMPTY = 0x2000;
35        const TONER_LOW = 0x4000;
36        const DOOR_OPEN = 0x8000;
37        const IDENTIFY_PRINTER_REQUESTED = 0x10000;
38    }
39}
40
41impl PrinterReason {
42    /// PWG keyword tokens for this flag set, in the order CUPS expects.
43    /// An empty set yields `["none"]`.
44    pub fn ipp_keywords(&self) -> Vec<&'static str> {
45        if self.is_empty() {
46            return vec!["none"];
47        }
48        let table = [
49            (Self::OTHER, "other"),
50            (Self::COVER_OPEN, "cover-open"),
51            (Self::DOOR_OPEN, "door-open"),
52            (Self::INPUT_TRAY_MISSING, "input-tray-missing"),
53            (Self::MARKER_SUPPLY_EMPTY, "marker-supply-empty"),
54            (Self::MARKER_SUPPLY_LOW, "marker-supply-low"),
55            (Self::MARKER_WASTE_ALMOST_FULL, "marker-waste-almost-full"),
56            (Self::MARKER_WASTE_FULL, "marker-waste-full"),
57            (Self::MEDIA_EMPTY, "media-empty"),
58            (Self::MEDIA_JAM, "media-jam"),
59            (Self::MEDIA_LOW, "media-low"),
60            (Self::MEDIA_NEEDED, "media-needed"),
61            (Self::OFFLINE, "offline-report"),
62            (Self::SPOOL_AREA_FULL, "spool-area-full"),
63            (Self::TONER_EMPTY, "toner-empty"),
64            (Self::TONER_LOW, "toner-low"),
65            (
66                Self::IDENTIFY_PRINTER_REQUESTED,
67                "identify-printer-requested",
68            ),
69        ];
70        table
71            .into_iter()
72            .filter(|(bit, _)| self.contains(*bit))
73            .map(|(_, kw)| kw)
74            .collect()
75    }
76
77    /// True if any set reason is a transient, user-clearable condition that
78    /// stops printing *now* but will let the job through once resolved —
79    /// offline, paper jam, out of media, cover/door open, supply empty.
80    ///
81    /// A device backend can use this to decide whether a print failure should
82    /// become [`crate::JobOutcome::DeviceUnavailable`] (the framework holds and
83    /// retries the job, like a printer holding a job through a jam) rather than
84    /// [`crate::JobOutcome::Failed`] (a permanent abort). A pure-`OTHER` or
85    /// empty reason set is *not* recoverable.
86    pub fn is_recoverable(self) -> bool {
87        self.intersects(
88            Self::OFFLINE
89                | Self::MEDIA_JAM
90                | Self::MEDIA_EMPTY
91                | Self::MEDIA_NEEDED
92                | Self::INPUT_TRAY_MISSING
93                | Self::COVER_OPEN
94                | Self::DOOR_OPEN
95                | Self::MARKER_SUPPLY_EMPTY
96                | Self::TONER_EMPTY,
97        )
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn empty_set_is_none() {
107        assert_eq!(PrinterReason::empty().ipp_keywords(), vec!["none"]);
108    }
109
110    #[test]
111    fn single_flag_surfaces() {
112        assert_eq!(PrinterReason::COVER_OPEN.ipp_keywords(), vec!["cover-open"]);
113    }
114
115    #[test]
116    fn multi_flag_surfaces_all() {
117        let r = PrinterReason::COVER_OPEN | PrinterReason::MEDIA_EMPTY;
118        let kws = r.ipp_keywords();
119        assert!(kws.contains(&"cover-open"));
120        assert!(kws.contains(&"media-empty"));
121        assert!(!kws.contains(&"none"));
122    }
123
124    #[test]
125    fn recoverable_conditions_hold_the_job() {
126        // Clearable physical conditions → the job should be held + retried.
127        for r in [
128            PrinterReason::OFFLINE,
129            PrinterReason::MEDIA_JAM,
130            PrinterReason::MEDIA_EMPTY,
131            PrinterReason::COVER_OPEN,
132            PrinterReason::MEDIA_NEEDED | PrinterReason::OTHER, // any recoverable bit counts
133        ] {
134            assert!(r.is_recoverable(), "{r:?} should be recoverable");
135        }
136        // A bare/unknown failure is a permanent abort, not a hold.
137        assert!(!PrinterReason::OTHER.is_recoverable());
138        assert!(!PrinterReason::empty().is_recoverable());
139    }
140}