unparser_shared/kind.rs
1//! Error-kind numbering for the `un*` family: the values that are already shared, and
2//! the bands that keep future ones from colliding.
3//!
4//! # What is here, and what deliberately is not
5//!
6//! Each library owns its own `ErrorKind` enum, because the reasons a PDF fails to parse
7//! are not the reasons a spreadsheet does. What they share is the *numbering* those enums
8//! live in, since the discriminants cross the C ABI as plain integers.
9//!
10//! This module therefore exports **only values that are already identical across the
11//! family**. It does not offer a broad set of "common kinds", and that restraint is the
12//! whole design: a library adopting a constant whose value differs from what it currently
13//! ships would be renumbering its public ABI. Above [`UNKNOWN_FORMAT`] the family already
14//! disagrees — the low band was assigned independently before anyone thought to align it —
15//! so any value published here would be a renumbering request aimed at somebody.
16//!
17//! For everything else, this module offers **bands** rather than values: a range each
18//! library allocates from, so that a kind added next year does not land on a number that
19//! already means something elsewhere.
20//!
21//! # Bands
22//!
23//! | Range | Meaning |
24//! |---|---|
25//! | `0` | Success. Never a valid kind — see [`NONE`] |
26//! | `1..=17` | Assigned before the convention existed. Frozen where shipped; see below |
27//! | [`COMMON_BAND`] | New reasons that genuinely apply to more than one library |
28//! | [`BOUNDARY_BAND`] | Failures of the ABI call itself, with no library-side counterpart |
29//! | `200..` | One band per library, [`LIBRARY_BAND_SIZE`] wide. See [`library_band`] |
30//!
31//! The `1..=17` range is history, not design. Numbers in it are frozen for any library
32//! that has shipped them, and a library that has *not* shipped a given number should not
33//! assume it is free to mean something new — a consumer reading two of these libraries
34//! sees one integer space. Add new reasons from [`COMMON_BAND`] or a library band.
35//!
36//! # Where a failure is attributed
37//!
38//! Two libraries classified the same failure differently — serialising a rendered result
39//! to JSON — because nothing said where it belonged. A shared numbering is only useful if
40//! the same failure lands on the same number, so the rule is stated here rather than
41//! rediscovered:
42//!
43//! **Failing to serialise a rendered result is a rendering failure.** Producing output is
44//! rendering, and it stays rendering when the last step of producing it is serialisation.
45//! It is not a generic failure, and it is not a boundary failure — nothing is wrong with
46//! how the call was made.
47//!
48//! A library with no rendering reason of its own takes one from its own band rather than
49//! reaching for [`OTHER`], which means "this failure carries no classification" and is
50//! worth keeping true.
51//!
52//! # The contract consumers are promised
53//!
54//! Every library in the family documents the same four rules. They are restated here
55//! because a shared numbering is only worth having if the rules are shared too:
56//!
57//! 1. A new reason takes a new number. Existing numbers are never reused or renumbered.
58//! 2. An unrecognised value is not an error — treat it as a generic failure and keep the
59//! number. A newer library stays usable by an older caller precisely because of this.
60//! 3. `0` means success, never a reason.
61//! 4. Reasons are `#[non_exhaustive]`: match with a `_ =>` arm.
62
63use std::ffi::c_int;
64use std::ops::RangeInclusive;
65
66/// No error is recorded: the last call on this thread succeeded.
67///
68/// Not a valid kind. Zero is reserved for this so that a zeroed struct or an
69/// uninitialised `int` cannot be mistaken for a classified failure.
70pub const NONE: c_int = 0;
71
72/// A failure with no more specific classification.
73///
74/// Exists so that bindings have a value for a failure that carries no classification —
75/// one raised by the binding itself, say. It must never be confused with success.
76///
77/// Shared across the family; adopting it is a no-op for libraries that already use `1`.
78pub const OTHER: c_int = 1;
79
80/// An I/O failure: a missing file, an unreadable one, a short read.
81///
82/// Shared across the family.
83pub const IO: c_int = 2;
84
85/// The input is not a document this library recognises at all.
86///
87/// Distinct from "recognised but unsupported", which is a library-specific reason.
88///
89/// Shared across the family.
90pub const UNKNOWN_FORMAT: c_int = 3;
91
92/// Reasons that apply to more than one library and were assigned after the convention.
93///
94/// A reason belongs here only once **two or more** libraries expose it. One library's
95/// need is served by its own band — a shared numbering earns its keep by describing what
96/// is actually shared, not by anticipating.
97pub const COMMON_BAND: RangeInclusive<c_int> = 18..=99;
98
99/// Reasons that describe a failure of the ABI call itself.
100///
101/// These have no library-side counterpart: nothing went wrong with the document, the
102/// call was made wrongly or could not deliver its result. The three values in this band
103/// are the ones the family already agrees on.
104pub const BOUNDARY_BAND: RangeInclusive<c_int> = 100..=199;
105
106/// An argument was null, or a string argument was not valid UTF-8.
107pub const INVALID_ARGUMENT: c_int = 100;
108
109/// A panic was caught at the ABI boundary.
110///
111/// Unwinding across `extern "C"` is undefined behaviour, so every entry point catches.
112/// Reaching this value means a bug in the library, not in the caller's input.
113pub const PANIC: c_int = 101;
114
115/// The result could not cross the ABI: a string holding an interior NUL byte.
116///
117/// The output exists and is correct as far as the library is concerned; it simply cannot
118/// be represented as a C string. Reported rather than truncated, because silently
119/// returning the text up to the NUL loses the rest without saying so.
120pub const INVALID_OUTPUT: c_int = 102;
121
122/// Width of each library's private band.
123pub const LIBRARY_BAND_SIZE: c_int = 100;
124
125/// First number available to library bands.
126pub const FIRST_LIBRARY_BAND: c_int = 200;
127
128/// The band belonging to library `index`, counting from zero.
129///
130/// Bands are handed out in a fixed order so that a number identifies which library
131/// produced it. The current allocation:
132///
133/// | Index | Library | Band |
134/// |---|---|---|
135/// | 0 | `unpdf` | `200..=299` |
136/// | 1 | `undoc` | `300..=399` |
137/// | 2 | `unhwp` | `400..=499` |
138///
139/// ```
140/// # use unparser_shared::kind;
141/// assert_eq!(kind::library_band(2), 400..=499);
142/// ```
143pub const fn library_band(index: c_int) -> RangeInclusive<c_int> {
144 let start = FIRST_LIBRARY_BAND + index * LIBRARY_BAND_SIZE;
145 start..=(start + LIBRARY_BAND_SIZE - 1)
146}
147
148/// Whether `kind` names a failure at the ABI boundary rather than in the document.
149///
150/// Useful to a binding that wants to report "you called this wrongly" differently from
151/// "your document is broken".
152///
153/// ```
154/// # use unparser_shared::kind;
155/// assert!(kind::is_boundary(kind::PANIC));
156/// assert!(!kind::is_boundary(kind::IO));
157/// assert!(!kind::is_boundary(kind::NONE));
158/// ```
159pub fn is_boundary(kind: c_int) -> bool {
160 BOUNDARY_BAND.contains(&kind)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 /// These four values are the family contract. A change here is a breaking change for
168 /// three published ABIs at once, so it is asserted rather than merely documented.
169 #[test]
170 fn the_shared_values_are_what_the_family_already_ships() {
171 assert_eq!(NONE, 0);
172 assert_eq!(OTHER, 1);
173 assert_eq!(IO, 2);
174 assert_eq!(UNKNOWN_FORMAT, 3);
175 assert_eq!(INVALID_ARGUMENT, 100);
176 assert_eq!(PANIC, 101);
177 assert_eq!(INVALID_OUTPUT, 102);
178 }
179
180 #[test]
181 fn bands_do_not_overlap_and_leave_the_historical_range_alone() {
182 assert!(*COMMON_BAND.start() > UNKNOWN_FORMAT);
183 assert_eq!(
184 *COMMON_BAND.start(),
185 18,
186 "1..=17 is history, not ours to hand out"
187 );
188 assert!(COMMON_BAND.end() < BOUNDARY_BAND.start());
189 assert!(*BOUNDARY_BAND.end() < FIRST_LIBRARY_BAND);
190 }
191
192 #[test]
193 fn boundary_values_live_in_the_boundary_band() {
194 for kind in [INVALID_ARGUMENT, PANIC, INVALID_OUTPUT] {
195 assert!(is_boundary(kind), "{kind} should be a boundary reason");
196 }
197 for kind in [NONE, OTHER, IO, UNKNOWN_FORMAT] {
198 assert!(!is_boundary(kind), "{kind} should not be a boundary reason");
199 }
200 }
201
202 #[test]
203 fn library_bands_are_adjacent_and_disjoint() {
204 let first = library_band(0);
205 let second = library_band(1);
206 let third = library_band(2);
207
208 assert_eq!(first, 200..=299);
209 assert_eq!(second, 300..=399);
210 assert_eq!(third, 400..=499);
211 assert_eq!(*second.start(), *first.end() + 1);
212 assert_eq!(*third.start(), *second.end() + 1);
213 assert!(!first.contains(second.start()));
214 }
215}