module_info/error.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::fmt;
5
6use cfg_if::cfg_if;
7
8/// Errors returned from `module_info` APIs.
9///
10/// `#[non_exhaustive]`: new variants may land in minor releases. Any `match`
11/// on this enum from outside the crate needs a wildcard arm or it will
12/// fail to compile when a variant is added.
13///
14/// # Example
15///
16/// ```
17/// use module_info::{ModuleInfoError, ModuleInfoResult};
18///
19/// // A function that might return a ModuleInfoError
20/// fn get_module_name() -> ModuleInfoResult<String> {
21/// Err(ModuleInfoError::NotAvailable("example".to_string()))
22/// }
23///
24/// match get_module_name() {
25/// Ok(name) => println!("Module name: {name}"),
26/// Err(ModuleInfoError::NotAvailable(msg)) => eprintln!("not available: {msg}"),
27/// Err(ModuleInfoError::NullPointer) => eprintln!("null pointer"),
28/// Err(ModuleInfoError::MalformedJson(msg)) => eprintln!("malformed JSON: {msg}"),
29/// Err(e) => eprintln!("other error: {e}"),
30/// }
31/// ```
32#[derive(Debug)]
33#[non_exhaustive]
34pub enum ModuleInfoError {
35 /// Module info is unavailable: either the `embed-module-info` feature is
36 /// off or the target is not Linux. The contained string carries context.
37 NotAvailable(String),
38
39 /// A null pointer was passed to `extract_module_info`. Typically means
40 /// the linker script did not run or the `.note.package` section was
41 /// stripped from the binary.
42 NullPointer,
43
44 /// The embedded bytes were not valid UTF-8.
45 Utf8Error(std::str::Utf8Error),
46
47 /// The embedded JSON could not be parsed, a required field is missing
48 /// or empty, or `moduleVersion` is not four `u16`-sized parts. The
49 /// contained string identifies the specific failure.
50 MalformedJson(String),
51
52 /// The serialized metadata JSON exceeded `MAX_JSON_SIZE` (1 KiB) at
53 /// build time. The contained string reports the actual vs. allowed size.
54 MetadataTooLarge(String),
55
56 /// I/O failure while reading `Cargo.toml` or writing the generated
57 /// linker script and JSON dump from `build.rs`.
58 IoError(std::io::Error),
59
60 /// Catch-all for errors that do not fit the variants above. Holds the
61 /// originating error for `source()` chaining.
62 Other(Box<dyn std::error::Error + Send + Sync>),
63}
64
65impl fmt::Display for ModuleInfoError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 ModuleInfoError::NotAvailable(msg) => write!(f, "Module info not available: {msg}"),
69 ModuleInfoError::NullPointer => write!(f, "Pointer is null"),
70 ModuleInfoError::Utf8Error(err) => write!(f, "UTF-8 conversion error: {err}"),
71 ModuleInfoError::MalformedJson(msg) => write!(f, "Malformed JSON string: {msg}"),
72 ModuleInfoError::MetadataTooLarge(msg) => {
73 write!(f, "Metadata size exceeds limit: {msg}")
74 }
75 ModuleInfoError::IoError(err) => write!(f, "IO error: {err}"),
76 ModuleInfoError::Other(err) => write!(f, "Other error: {err}"),
77 }
78 }
79}
80
81impl std::error::Error for ModuleInfoError {
82 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
83 match self {
84 ModuleInfoError::Utf8Error(err) => Some(err),
85 ModuleInfoError::IoError(err) => Some(err),
86 ModuleInfoError::Other(err) => Some(err.as_ref()),
87 _ => None,
88 }
89 }
90}
91
92impl From<std::str::Utf8Error> for ModuleInfoError {
93 fn from(err: std::str::Utf8Error) -> Self {
94 ModuleInfoError::Utf8Error(err)
95 }
96}
97
98impl From<std::io::Error> for ModuleInfoError {
99 fn from(err: std::io::Error) -> Self {
100 ModuleInfoError::IoError(err)
101 }
102}
103
104impl From<std::env::VarError> for ModuleInfoError {
105 fn from(err: std::env::VarError) -> Self {
106 ModuleInfoError::Other(Box::new(err))
107 }
108}
109
110// Conditionally compile the toml and serde_json error conversions only for Linux
111cfg_if! {
112 if #[cfg(target_os = "linux")] {
113 impl From<toml::de::Error> for ModuleInfoError {
114 fn from(err: toml::de::Error) -> Self {
115 ModuleInfoError::Other(Box::new(err))
116 }
117 }
118
119 impl From<serde_json::Error> for ModuleInfoError {
120 fn from(err: serde_json::Error) -> Self {
121 ModuleInfoError::Other(Box::new(err))
122 }
123 }
124 }
125}
126
127/// A type alias for Results that use ModuleInfoError
128pub type ModuleInfoResult<T> = Result<T, ModuleInfoError>;
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use std::error::Error as _;
134
135 /// Sweep every variant once: build it, run `Display::fmt`, and call
136 /// `Error::source`. Together this exercises every match arm in
137 /// `Display::fmt` and every arm in `source()` that returns a
138 /// concrete error vs. `None`. Without it `error.rs` shows 0%
139 /// coverage in `cargo llvm-cov` because variants are *constructed*
140 /// elsewhere via `?`/`From` impls but never *displayed*.
141 #[test]
142 fn display_and_source_cover_every_variant() {
143 // Use a runtime-built byte slice so `clippy::invalid_utf8_in_unchecked`
144 // (and the related `invalid-from-utf8` lint that fires on a literal
145 // `&[0xff]`) doesn't flag the test as obviously-erroring at compile
146 // time. The bytes are still always invalid UTF-8 at runtime.
147 let invalid_utf8: Vec<u8> = vec![0xff, 0xfe];
148 let utf8_err = match std::str::from_utf8(&invalid_utf8) {
149 Ok(_) => unreachable!("invalid_utf8 is never valid UTF-8"),
150 Err(e) => e,
151 };
152
153 // For each variant, assert (a) Display produces a non-empty
154 // string with the expected prefix, and (b) `source()` returns
155 // Some/None per the doc contract.
156 let cases: Vec<(ModuleInfoError, &str, bool)> = vec![
157 (
158 ModuleInfoError::NotAvailable("ctx".into()),
159 "Module info not available",
160 false,
161 ),
162 (ModuleInfoError::NullPointer, "Pointer is null", false),
163 (
164 ModuleInfoError::MalformedJson("bad".into()),
165 "Malformed JSON string",
166 false,
167 ),
168 (
169 ModuleInfoError::MetadataTooLarge("size".into()),
170 "Metadata size exceeds limit",
171 false,
172 ),
173 // Variants whose source() returns the inner error:
174 (
175 ModuleInfoError::Utf8Error(utf8_err),
176 "UTF-8 conversion error",
177 true,
178 ),
179 (
180 ModuleInfoError::IoError(std::io::Error::new(
181 std::io::ErrorKind::NotFound,
182 "missing",
183 )),
184 "IO error",
185 true,
186 ),
187 (
188 ModuleInfoError::Other(std::io::Error::other("boxed").into()),
189 "Other error",
190 true,
191 ),
192 ];
193 for (err, prefix, has_source) in cases {
194 let rendered = format!("{err}");
195 assert!(
196 rendered.starts_with(prefix),
197 "Display for {err:?} should start with {prefix:?}, got {rendered:?}"
198 );
199 assert_eq!(
200 err.source().is_some(),
201 has_source,
202 "source() arm wrong for {err:?}"
203 );
204 }
205 }
206
207 /// `From<std::str::Utf8Error>` and `From<std::io::Error>` are the
208 /// auto-conversions `?` exercises throughout the crate. Hit them
209 /// directly here so coverage doesn't silently drop if a future
210 /// refactor stops using `?` against those error types in the
211 /// production paths these tests instrument.
212 #[test]
213 fn from_impls_wrap_into_correct_variant() {
214 let invalid_utf8: Vec<u8> = vec![0xff];
215 let utf8_err = match std::str::from_utf8(&invalid_utf8) {
216 Ok(_) => unreachable!("invalid_utf8 is never valid UTF-8"),
217 Err(e) => e,
218 };
219 let wrapped: ModuleInfoError = utf8_err.into();
220 assert!(matches!(wrapped, ModuleInfoError::Utf8Error(_)));
221
222 let io_err = std::io::Error::other("x");
223 let wrapped: ModuleInfoError = io_err.into();
224 assert!(matches!(wrapped, ModuleInfoError::IoError(_)));
225
226 // VarError uses the catch-all `Other` arm, not a dedicated
227 // variant. Pin that contract so a refactor doesn't accidentally
228 // promote it to its own variant without updating callers.
229 let var_err = std::env::VarError::NotPresent;
230 let wrapped: ModuleInfoError = var_err.into();
231 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
232 }
233
234 /// On Linux, `toml::de::Error` and `serde_json::Error` also have
235 /// `From` impls (used by `Cargo.toml` parsing and JSON validation).
236 /// Cover those arms too. Gated on Linux because the impls are
237 /// `#[cfg(target_os = "linux")]`.
238 #[cfg(target_os = "linux")]
239 #[test]
240 fn linux_from_impls_wrap_into_other() {
241 let toml_err = toml::from_str::<toml::Value>("not [valid").unwrap_err();
242 let wrapped: ModuleInfoError = toml_err.into();
243 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
244
245 let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
246 let wrapped: ModuleInfoError = json_err.into();
247 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
248 }
249}