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 std::error::Error as _;
133
134 use super::*;
135
136 /// Sweep every variant once: build it, run `Display::fmt`, and call
137 /// `Error::source`. Together this exercises every match arm in
138 /// `Display::fmt` and every arm in `source()` that returns a
139 /// concrete error vs. `None`. Without it `error.rs` shows 0%
140 /// coverage in `cargo llvm-cov` because variants are *constructed*
141 /// elsewhere via `?`/`From` impls but never *displayed*.
142 #[test]
143 fn display_and_source_cover_every_variant() {
144 // Use a runtime-built byte slice so `clippy::invalid_utf8_in_unchecked`
145 // (and the related `invalid-from-utf8` lint that fires on a literal
146 // `&[0xff]`) doesn't flag the test as obviously-erroring at compile
147 // time. The bytes are still always invalid UTF-8 at runtime.
148 let invalid_utf8: Vec<u8> = vec![0xff, 0xfe];
149 let utf8_err = match std::str::from_utf8(&invalid_utf8) {
150 Ok(_) => unreachable!("invalid_utf8 is never valid UTF-8"),
151 Err(e) => e,
152 };
153
154 // For each variant, assert (a) Display produces a non-empty
155 // string with the expected prefix, and (b) `source()` returns
156 // Some/None per the doc contract.
157 let cases: Vec<(ModuleInfoError, &str, bool)> = vec![
158 (
159 ModuleInfoError::NotAvailable("ctx".into()),
160 "Module info not available",
161 false,
162 ),
163 (ModuleInfoError::NullPointer, "Pointer is null", false),
164 (
165 ModuleInfoError::MalformedJson("bad".into()),
166 "Malformed JSON string",
167 false,
168 ),
169 (
170 ModuleInfoError::MetadataTooLarge("size".into()),
171 "Metadata size exceeds limit",
172 false,
173 ),
174 // Variants whose source() returns the inner error:
175 (
176 ModuleInfoError::Utf8Error(utf8_err),
177 "UTF-8 conversion error",
178 true,
179 ),
180 (
181 ModuleInfoError::IoError(std::io::Error::new(
182 std::io::ErrorKind::NotFound,
183 "missing",
184 )),
185 "IO error",
186 true,
187 ),
188 (
189 ModuleInfoError::Other(std::io::Error::other("boxed").into()),
190 "Other error",
191 true,
192 ),
193 ];
194 for (err, prefix, has_source) in cases {
195 let rendered = format!("{err}");
196 assert!(
197 rendered.starts_with(prefix),
198 "Display for {err:?} should start with {prefix:?}, got {rendered:?}"
199 );
200 assert_eq!(
201 err.source().is_some(),
202 has_source,
203 "source() arm wrong for {err:?}"
204 );
205 }
206 }
207
208 /// `From<std::str::Utf8Error>` and `From<std::io::Error>` are the
209 /// auto-conversions `?` exercises throughout the crate. Hit them
210 /// directly here so coverage doesn't silently drop if a future
211 /// refactor stops using `?` against those error types in the
212 /// production paths these tests instrument.
213 #[test]
214 fn from_impls_wrap_into_correct_variant() {
215 let invalid_utf8: Vec<u8> = vec![0xff];
216 let utf8_err = match std::str::from_utf8(&invalid_utf8) {
217 Ok(_) => unreachable!("invalid_utf8 is never valid UTF-8"),
218 Err(e) => e,
219 };
220 let wrapped: ModuleInfoError = utf8_err.into();
221 assert!(matches!(wrapped, ModuleInfoError::Utf8Error(_)));
222
223 let io_err = std::io::Error::other("x");
224 let wrapped: ModuleInfoError = io_err.into();
225 assert!(matches!(wrapped, ModuleInfoError::IoError(_)));
226
227 // VarError uses the catch-all `Other` arm, not a dedicated
228 // variant. Pin that contract so a refactor doesn't accidentally
229 // promote it to its own variant without updating callers.
230 let var_err = std::env::VarError::NotPresent;
231 let wrapped: ModuleInfoError = var_err.into();
232 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
233 }
234
235 /// On Linux, `toml::de::Error` and `serde_json::Error` also have
236 /// `From` impls (used by `Cargo.toml` parsing and JSON validation).
237 /// Cover those arms too. Gated on Linux because the impls are
238 /// `#[cfg(target_os = "linux")]`.
239 #[cfg(target_os = "linux")]
240 #[test]
241 fn linux_from_impls_wrap_into_other() {
242 let toml_err = toml::from_str::<toml::Value>("not [valid").unwrap_err();
243 let wrapped: ModuleInfoError = toml_err.into();
244 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
245
246 let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
247 let wrapped: ModuleInfoError = json_err.into();
248 assert!(matches!(wrapped, ModuleInfoError::Other(_)));
249 }
250}