Skip to main content

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}