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 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}