1use std::{
4 error::Error,
5 fmt,
6 io::{self, ErrorKind},
7};
8
9use monty_types::{ExcData, ExcType, MontyException, StringRepr, unicode_decode_error_msg};
10
11#[derive(Debug)]
13pub enum MountError {
14 NoMountPoint(String),
16
17 PathEscape {
20 virtual_path: String,
22 },
23
24 EmbeddedNullByte(&'static str),
32
33 ReadOnly(String),
35
36 CrossMountRename {
38 src: String,
40 dst: String,
42 },
43
44 Io(io::Error, String),
46
47 InvalidUtf8 {
51 start: usize,
53 end: usize,
56 first_byte: u8,
58 reason: &'static str,
60 data: ExcData,
64 },
65
66 InvalidMount(String),
68
69 WriteLimitExceeded(u64),
72
73 MemoryUsageLimitExceeded(u64),
76}
77
78impl MountError {
79 #[must_use]
81 pub fn into_exception(self) -> MontyException {
82 match self {
83 Self::NoMountPoint(path) => MontyException::new(
84 ExcType::PermissionError,
85 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&path))),
86 ),
87 Self::PathEscape { virtual_path } => MontyException::new(
88 ExcType::PermissionError,
89 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&virtual_path))),
90 ),
91 Self::EmbeddedNullByte(message) => MontyException::new(ExcType::ValueError, Some(message.to_owned())),
95 Self::ReadOnly(path) => MontyException::new(
96 ExcType::PermissionError,
97 Some(format!("[Errno 30] Read-only file system: {}", StringRepr(&path))),
98 ),
99 Self::CrossMountRename { src, dst } => MontyException::new(
100 ExcType::OSError,
101 Some(format!(
102 "[Errno 18] Invalid cross-device link: {} -> {}",
103 StringRepr(&src),
104 StringRepr(&dst)
105 )),
106 ),
107 Self::Io(err, path) => match err.kind() {
112 ErrorKind::NotFound => MontyException::new(
113 ExcType::FileNotFoundError,
114 Some(format!("[Errno 2] No such file or directory: {}", StringRepr(&path))),
115 ),
116 ErrorKind::AlreadyExists => MontyException::new(
117 ExcType::FileExistsError,
118 Some(format!("[Errno 17] File exists: {}", StringRepr(&path))),
119 ),
120 ErrorKind::PermissionDenied => MontyException::new(
121 ExcType::PermissionError,
122 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&path))),
123 ),
124 ErrorKind::IsADirectory => MontyException::new(
125 ExcType::IsADirectoryError,
126 Some(format!("[Errno 21] Is a directory: {}", StringRepr(&path))),
127 ),
128 ErrorKind::NotADirectory => MontyException::new(
129 ExcType::NotADirectoryError,
130 Some(format!("[Errno 20] Not a directory: {}", StringRepr(&path))),
131 ),
132 ErrorKind::DirectoryNotEmpty => MontyException::new(
133 ExcType::OSError,
134 Some(format!("[Errno 39] Directory not empty: {}", StringRepr(&path))),
135 ),
136 ErrorKind::InvalidFilename => MontyException::new(
137 ExcType::OSError,
138 Some(format!("[Errno 36] File name too long: {}", StringRepr(&path))),
139 ),
140 _ => MontyException::new(ExcType::OSError, Some(format!("{err}: {}", StringRepr(&path)))),
141 },
142 Self::InvalidUtf8 {
143 start,
144 end,
145 first_byte,
146 reason,
147 data,
148 } => MontyException::new(
149 ExcType::UnicodeDecodeError,
150 Some(unicode_decode_error_msg("utf-8", first_byte, start, end, reason)),
151 )
152 .with_data(data),
153 Self::InvalidMount(msg) => MontyException::new(ExcType::TypeError, Some(msg)),
154 Self::WriteLimitExceeded(limit) => MontyException::new(
155 ExcType::OSError,
156 Some(format!("disk write limit of {} exceeded", format_bytes_pretty(limit))),
157 ),
158 Self::MemoryUsageLimitExceeded(limit) => MontyException::new(
159 ExcType::MemoryError,
160 Some(format!(
161 "mount memory usage limit of {} exceeded",
162 format_bytes_pretty(limit)
163 )),
164 ),
165 }
166 }
167
168 pub(super) fn io_err(kind: ErrorKind, msg: &str, vpath: &str) -> Self {
170 Self::Io(io::Error::new(kind, msg), vpath.to_owned())
171 }
172
173 pub(super) fn not_found(vpath: &str) -> Self {
175 Self::io_err(ErrorKind::NotFound, "No such file or directory", vpath)
176 }
177}
178
179impl fmt::Display for MountError {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 Self::NoMountPoint(path) => write!(f, "no mount point for path: {path}"),
183 Self::PathEscape { virtual_path } => write!(f, "path escape detected: {virtual_path}"),
184 Self::EmbeddedNullByte(message) => write!(f, "{message}"),
185 Self::ReadOnly(path) => write!(f, "read-only mount: {path}"),
186 Self::CrossMountRename { src, dst } => write!(f, "cross-mount rename: {src} -> {dst}"),
187 Self::Io(err, path) => write!(f, "I/O error on {path}: {err}"),
188 Self::InvalidUtf8 { start, first_byte, .. } => {
189 write!(f, "invalid UTF-8 byte 0x{first_byte:02x} at position {start}")
190 }
191 Self::InvalidMount(msg) => write!(f, "invalid mount: {msg}"),
192 Self::WriteLimitExceeded(limit) => {
193 write!(f, "disk write limit of {} exceeded", format_bytes_pretty(*limit))
194 }
195 Self::MemoryUsageLimitExceeded(limit) => {
196 write!(
197 f,
198 "mount memory usage limit of {} exceeded",
199 format_bytes_pretty(*limit)
200 )
201 }
202 }
203 }
204}
205
206impl Error for MountError {
207 fn source(&self) -> Option<&(dyn Error + 'static)> {
208 match self {
209 Self::Io(err, _) => Some(err),
210 _ => None,
211 }
212 }
213}
214
215fn format_bytes_pretty(bytes: u64) -> String {
222 const KB: u64 = 1_000;
223 const MB: u64 = 1_000_000;
224 const GB: u64 = 1_000_000_000;
225 const TB: u64 = 1_000_000_000_000;
226
227 if bytes < KB {
228 return format!("{bytes} bytes");
229 }
230
231 let (value, unit) = if bytes < MB {
232 (bytes as f64 / KB as f64, "KB")
233 } else if bytes < GB {
234 (bytes as f64 / MB as f64, "MB")
235 } else if bytes < TB {
236 (bytes as f64 / GB as f64, "GB")
237 } else {
238 (bytes as f64 / TB as f64, "TB")
239 };
240
241 let tenths = (value * 10.0).round() % 10.0;
243 if tenths < f64::EPSILON {
244 format!("{value:.0} {unit}")
245 } else {
246 format!("{value:.1} {unit}")
247 }
248}