1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright 2024 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0
pub type Result<T> = std::result::Result<T, Error>;
/// Crate-wide error type.
///
/// `#[non_exhaustive]` because this enum is re-exported from a published
/// library: downstream `match`es must keep a wildcard arm so future
/// variants are not semver-breaking.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("System error: {0}")]
Errno(#[from] rustix::io::Errno),
#[error("Property not found: {0}")]
NotFound(String),
#[error("Encoding error: {0}")]
Encoding(String),
/// UTF-8 decode failure. A dedicated `#[from]` variant (rather than
/// stringifying into [`Error::Encoding`]) so `source()` still reaches
/// the original [`std::str::Utf8Error`].
///
/// Deliberately NOT mirrored by a `String::from_utf8` variant:
/// [`std::string::FromUtf8Error`] owns the failed byte buffer, and
/// carrying wire *values* inside errors would violate the service's
/// don't-log-values policy the moment someone logs `{e:?}`. Call sites
/// convert with `.map_err(|e| Error::Utf8(e.utf8_error()))`, which
/// keeps the diagnostic position info and drops the bytes.
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::str::Utf8Error),
/// Text that could not be parsed: malformed property names, bad
/// property-info lines, and — via `get<T>` — values whose `FromStr`
/// failed. This is the **single** parse-failure variant of the public
/// API: `get<T>`'s `T::Err` is only bound by `Display`, so the cause
/// is flattened into the message rather than carried as `source()`.
/// (A `ParseInt(ParseIntError)` sibling variant used to exist but was
/// never constructed — one variant, one contract.)
#[error("Parse error: {0}")]
Parse(String),
#[error("File validation error: {0}")]
FileValidation(String),
/// Caller-supplied argument violated an API contract (over-long
/// name/value, malformed input) — distinct from [`Error::FileValidation`],
/// which reports corrupt on-disk state.
#[error("Invalid argument: {0}")]
InvalidArgument(String),
/// A first-write-wins global (properties/socket directory) was already
/// initialized — explicitly via `init()`/`try_init()` or implicitly by
/// the first property access latching the default. Also used for
/// initialization-order conflicts of the same shape, e.g. a property
/// area already mapped read-only when a writable mapping is requested.
#[error("Already initialized: {0}")]
AlreadyInitialized(String),
/// A global resource budget was exhausted (e.g. the build.prop import
/// loader's total-file-loads budget). Deliberately NOT used for the
/// per-import recursion-depth cap, which is logged and skipped rather
/// than aborting. A dedicated variant — not [`Error::Parse`] — so
/// callers can distinguish "abort the whole load" limits from ordinary
/// per-item failures with `matches!` instead of re-inspecting counters
/// or message text.
#[error("Limit exceeded: {0}")]
LimitExceeded(String),
/// The property service accepted the connection but rejected the
/// request at the protocol level — the socket itself is healthy, so
/// this is deliberately not an [`Error::Io`].
#[error("Property service rejected \"{name}\": error code {code:#x}")]
ServiceError { name: String, code: i32 },
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("File size error: {0}")]
FileSize(String),
/// The fixed-size property area has no room for another allocation —
/// an operational limit (bionic returns `false` here), distinct from
/// the corrupt-file conditions reported as [`Error::FileValidation`] /
/// [`Error::FileSize`].
#[error("Property area full: {0}")]
AreaFull(String),
#[error("File ownership error: {0}")]
FileOwnership(String),
#[error("Lock error: {0}")]
Lock(String),
/// Cached global-initialization failure (see `try_system_properties`).
/// Wraps the original in `Arc` because the `OnceLock` cache can only
/// hand out references while callers need an owned value — the
/// original variant stays reachable via `source()`/`Arc` and its own
/// chain is preserved.
///
/// Like `Context` below, the source appears in `Display` *and* via
/// `#[source]` (duplicated text under chain-walking reporters) so that
/// bare `{e}` log sites still show the root cause.
#[error("SystemProperties initialization failed: {0}")]
Init(#[source] std::sync::Arc<Error>),
/// A wrapped error with caller-supplied context and the source error
/// preserved for `Error::source()` chain traversal.
///
/// Note: the source is deliberately included in `Display` *and*
/// exposed via `#[source]`, deviating from the thiserror convention of
/// one-or-the-other. This crate's log sites print bare `{e}`, so
/// dropping the source from `Display` would silence root causes at
/// every existing call site; the cost is a duplicated message when a
/// reporter walks the chain (anyhow-style "caused by" output).
#[error("{msg} (at {location}): {source}")]
Context {
msg: String,
location: &'static std::panic::Location<'static>,
#[source]
source: Box<Error>,
},
}
pub trait ContextWithLocation<T> {
#[track_caller]
fn context_with_location(self, msg: impl Into<String>) -> Result<T>;
/// Lazy variant of [`Self::context_with_location`]: the message closure
/// runs only on the error path. Use this in loops / hot paths where an
/// eagerly-evaluated `format!` argument would allocate on every
/// success.
#[track_caller]
fn with_context_location(self, f: impl FnOnce() -> String) -> Result<T>;
}
impl<T, E> ContextWithLocation<T> for std::result::Result<T, E>
where
E: Into<Error>,
{
#[track_caller]
fn context_with_location(self, msg: impl Into<String>) -> Result<T> {
let location = std::panic::Location::caller();
self.map_err(|e| Error::Context {
msg: msg.into(),
location,
source: Box::new(e.into()),
})
}
#[track_caller]
fn with_context_location(self, f: impl FnOnce() -> String) -> Result<T> {
let location = std::panic::Location::caller();
self.map_err(|e| Error::Context {
msg: f(),
location,
source: Box::new(e.into()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_io_via_question_mark() {
let err = try_open_file().unwrap_err();
assert!(matches!(err, Error::Io(_)));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn test_error_context_with_location() {
let err: Error = std::fs::File::open("non-existent-file")
.context_with_location("opening test file")
.unwrap_err();
assert!(matches!(err, Error::Context { .. }));
let msg = format!("{err}");
assert!(msg.contains("opening test file"));
assert!(msg.contains("non-existent-file") || msg.contains("No such"));
assert!(std::error::Error::source(&err).is_some());
}
fn try_open_file() -> Result<()> {
std::fs::File::open("non-existent-file")?;
Ok(())
}
}