Skip to main content

rsproperties/
errors.rs

1// Copyright 2024 Jeff Kim <hiking90@gmail.com>
2// SPDX-License-Identifier: Apache-2.0
3
4use std::num::ParseIntError;
5
6pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10    #[error("I/O error: {0}")]
11    Io(std::io::Error),
12
13    #[error("System error: {0}")]
14    Errno(rustix::io::Errno),
15
16    #[error("Property not found: {0}")]
17    NotFound(String),
18
19    #[error("Encoding error: {0}")]
20    Encoding(String),
21
22    #[error("Parse error: {0}")]
23    Parse(String),
24
25    #[error("File validation error: {0}")]
26    FileValidation(String),
27
28    #[error("Conversion error: {0}")]
29    Conversion(String),
30
31    #[error("Permission denied: {0}")]
32    PermissionDenied(String),
33
34    #[error("File size error: {0}")]
35    FileSize(String),
36
37    #[error("File ownership error: {0}")]
38    FileOwnership(String),
39
40    #[error("Lock error: {0}")]
41    LockError(String),
42}
43
44impl Error {
45    pub fn new_not_found(key: String) -> Error {
46        Error::NotFound(key)
47    }
48
49    pub fn new_encoding(msg: String) -> Error {
50        Error::Encoding(msg)
51    }
52
53    pub fn new_parse(msg: String) -> Error {
54        Error::Parse(msg)
55    }
56
57    pub fn new_file_validation(msg: String) -> Error {
58        Error::FileValidation(msg)
59    }
60
61    pub fn new_conversion(msg: String) -> Error {
62        Error::Conversion(msg)
63    }
64
65    pub fn new_permission_denied(msg: String) -> Error {
66        Error::PermissionDenied(msg)
67    }
68
69    pub fn new_file_size(msg: String) -> Error {
70        Error::FileSize(msg)
71    }
72
73    pub fn new_file_ownership(msg: String) -> Error {
74        Error::FileOwnership(msg)
75    }
76
77    pub fn new_lock_error(msg: String) -> Error {
78        Error::LockError(msg)
79    }
80
81    pub fn new_io(io_error: std::io::Error) -> Error {
82        let error = Error::Io(io_error);
83        log::error!("I/O error: {error}");
84        error
85    }
86
87    pub fn new_errno(errno: rustix::io::Errno) -> Error {
88        let error = Error::Errno(errno);
89        log::error!("System error: {error}");
90        error
91    }
92}
93
94impl From<rustix::io::Errno> for Error {
95    fn from(source: rustix::io::Errno) -> Self {
96        let error = Error::Errno(source);
97        log::error!("Converting errno to Error: {source}");
98        error
99    }
100}
101
102impl From<std::io::Error> for Error {
103    fn from(source: std::io::Error) -> Self {
104        let error = Error::Io(source);
105        log::error!("Converting I/O error to Error: {error}");
106        error
107    }
108}
109
110impl From<std::str::Utf8Error> for Error {
111    fn from(source: std::str::Utf8Error) -> Self {
112        let error_msg = format!("UTF-8 conversion error: {source}");
113        log::error!("{error_msg}");
114        Error::Encoding(error_msg)
115    }
116}
117
118impl From<std::ffi::OsString> for Error {
119    fn from(source: std::ffi::OsString) -> Self {
120        let error_msg = format!("OsString conversion error: {source:?}");
121        log::error!("{error_msg}");
122        Error::Conversion(error_msg)
123    }
124}
125
126impl From<&str> for Error {
127    fn from(source: &str) -> Self {
128        log::error!("String error: {source}");
129        Error::Parse(source.to_owned())
130    }
131}
132
133impl From<ParseIntError> for Error {
134    fn from(source: ParseIntError) -> Self {
135        let error_msg = format!("Parse integer error: {source}");
136        log::error!("{error_msg}");
137        Error::Parse(error_msg)
138    }
139}
140
141pub trait ContextWithLocation<T> {
142    fn context_with_location(self, msg: impl Into<String>) -> Result<T>;
143}
144
145impl<T, E> ContextWithLocation<T> for std::result::Result<T, E>
146where
147    E: Into<Error>,
148{
149    fn context_with_location(self, msg: impl Into<String>) -> Result<T> {
150        self.map_err(|e| e.into())
151            .map_err(|_| Error::new_file_validation(msg.into()))
152    }
153}
154
155/// Validates file metadata for system property files.
156///
157/// In test and debug modes, only checks file permissions and size.
158/// In production mode, also enforces that the file is owned by root (uid=0, gid=0).
159pub fn validate_file_metadata(
160    metadata: &std::fs::Metadata,
161    path: &std::path::Path,
162    min_size: u64,
163) -> Result<()> {
164    // Platform-specific MetadataExt imports
165    #[cfg(target_os = "android")]
166    use std::os::android::fs::MetadataExt;
167    #[cfg(target_os = "linux")]
168    use std::os::linux::fs::MetadataExt;
169    #[cfg(target_os = "macos")]
170    use std::os::macos::fs::MetadataExt;
171
172    use rustix::fs;
173
174    // Check file size first (applies to all modes)
175    if metadata.st_size() < min_size {
176        let error_msg = format!(
177            "File too small: size={}, min_size={} for {:?}",
178            metadata.st_size(),
179            min_size,
180            path
181        );
182        log::error!("{error_msg}");
183        return Err(Error::new_file_size(error_msg));
184    }
185
186    #[cfg(any(target_os = "android", target_os = "linux"))]
187    let check_permissions = metadata.st_mode() & (fs::Mode::WGRP.bits() | fs::Mode::WOTH.bits());
188
189    #[cfg(target_os = "macos")]
190    let check_permissions =
191        metadata.st_mode() & (fs::Mode::WGRP.bits() | fs::Mode::WOTH.bits()) as u32;
192
193    // Check write permissions (applies to all modes)
194    if check_permissions != 0 {
195        let error_msg = format!(
196            "File has group or other write permissions: mode={:#o} for {:?}",
197            metadata.st_mode(),
198            path
199        );
200        log::error!("{error_msg}");
201        return Err(Error::new_permission_denied(error_msg));
202    }
203
204    // In production mode, also check ownership
205    // Skip ownership checks only in test/development environments:
206    // 1. When compiled with debug assertions (development builds)
207    // 2. When compiled in test configuration
208    // This is compile-time only and cannot be bypassed at runtime
209    let skip_ownership_check = cfg!(debug_assertions) || cfg!(test);
210
211    if !skip_ownership_check && (metadata.st_uid() != 0 || metadata.st_gid() != 0) {
212        let error_msg = format!(
213            "File not owned by root: uid={}, gid={} for {:?}",
214            metadata.st_uid(),
215            metadata.st_gid(),
216            path
217        );
218        log::error!("{error_msg}");
219        return Err(Error::new_file_ownership(error_msg));
220    }
221
222    Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use anyhow::Context;
229
230    fn try_open_file() -> Result<()> {
231        std::fs::File::open("non-existent-file")?;
232        Ok(())
233    }
234
235    #[test]
236    fn test_error_location() {
237        try_open_file()
238            .map_err(|e| {
239                println!("Error: {e}");
240                e
241            })
242            .unwrap_err();
243        std::fs::File::open("non-existent-file")
244            .context("Failed to open file")
245            .unwrap_err();
246    }
247}