1use 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
155pub fn validate_file_metadata(
160 metadata: &std::fs::Metadata,
161 path: &std::path::Path,
162 min_size: u64,
163) -> Result<()> {
164 #[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 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 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 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}