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 ReadOnly(String),
26
27 CrossMountRename {
29 src: String,
31 dst: String,
33 },
34
35 Io(io::Error, String),
37
38 InvalidUtf8 {
42 start: usize,
44 end: usize,
47 first_byte: u8,
49 reason: &'static str,
51 data: ExcData,
55 },
56
57 InvalidMount(String),
59
60 WriteLimitExceeded(u64),
63
64 MemoryUsageLimitExceeded(u64),
67}
68
69impl MountError {
70 #[must_use]
72 pub fn into_exception(self) -> MontyException {
73 match self {
74 Self::NoMountPoint(path) => MontyException::new(
75 ExcType::PermissionError,
76 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&path))),
77 ),
78 Self::PathEscape { virtual_path } => MontyException::new(
79 ExcType::PermissionError,
80 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&virtual_path))),
81 ),
82 Self::ReadOnly(path) => MontyException::new(
83 ExcType::PermissionError,
84 Some(format!("[Errno 30] Read-only file system: {}", StringRepr(&path))),
85 ),
86 Self::CrossMountRename { src, dst } => MontyException::new(
87 ExcType::OSError,
88 Some(format!(
89 "[Errno 18] Invalid cross-device link: {} -> {}",
90 StringRepr(&src),
91 StringRepr(&dst)
92 )),
93 ),
94 Self::Io(err, path) => match err.kind() {
99 ErrorKind::NotFound => MontyException::new(
100 ExcType::FileNotFoundError,
101 Some(format!("[Errno 2] No such file or directory: {}", StringRepr(&path))),
102 ),
103 ErrorKind::AlreadyExists => MontyException::new(
104 ExcType::FileExistsError,
105 Some(format!("[Errno 17] File exists: {}", StringRepr(&path))),
106 ),
107 ErrorKind::PermissionDenied => MontyException::new(
108 ExcType::PermissionError,
109 Some(format!("[Errno 13] Permission denied: {}", StringRepr(&path))),
110 ),
111 ErrorKind::IsADirectory => MontyException::new(
112 ExcType::IsADirectoryError,
113 Some(format!("[Errno 21] Is a directory: {}", StringRepr(&path))),
114 ),
115 ErrorKind::NotADirectory => MontyException::new(
116 ExcType::NotADirectoryError,
117 Some(format!("[Errno 20] Not a directory: {}", StringRepr(&path))),
118 ),
119 ErrorKind::DirectoryNotEmpty => MontyException::new(
120 ExcType::OSError,
121 Some(format!("[Errno 39] Directory not empty: {}", StringRepr(&path))),
122 ),
123 ErrorKind::InvalidFilename => MontyException::new(
124 ExcType::OSError,
125 Some(format!("[Errno 36] File name too long: {}", StringRepr(&path))),
126 ),
127 _ => MontyException::new(ExcType::OSError, Some(format!("{err}: {}", StringRepr(&path)))),
128 },
129 Self::InvalidUtf8 {
130 start,
131 end,
132 first_byte,
133 reason,
134 data,
135 } => MontyException::new(
136 ExcType::UnicodeDecodeError,
137 Some(unicode_decode_error_msg("utf-8", first_byte, start, end, reason)),
138 )
139 .with_data(data),
140 Self::InvalidMount(msg) => MontyException::new(ExcType::TypeError, Some(msg)),
141 Self::WriteLimitExceeded(limit) => MontyException::new(
142 ExcType::OSError,
143 Some(format!("disk write limit of {} exceeded", format_bytes_pretty(limit))),
144 ),
145 Self::MemoryUsageLimitExceeded(limit) => MontyException::new(
146 ExcType::MemoryError,
147 Some(format!(
148 "mount memory usage limit of {} exceeded",
149 format_bytes_pretty(limit)
150 )),
151 ),
152 }
153 }
154
155 pub(super) fn io_err(kind: ErrorKind, msg: &str, vpath: &str) -> Self {
157 Self::Io(io::Error::new(kind, msg), vpath.to_owned())
158 }
159
160 pub(super) fn not_found(vpath: &str) -> Self {
162 Self::io_err(ErrorKind::NotFound, "No such file or directory", vpath)
163 }
164}
165
166impl fmt::Display for MountError {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::NoMountPoint(path) => write!(f, "no mount point for path: {path}"),
170 Self::PathEscape { virtual_path } => write!(f, "path escape detected: {virtual_path}"),
171 Self::ReadOnly(path) => write!(f, "read-only mount: {path}"),
172 Self::CrossMountRename { src, dst } => write!(f, "cross-mount rename: {src} -> {dst}"),
173 Self::Io(err, path) => write!(f, "I/O error on {path}: {err}"),
174 Self::InvalidUtf8 { start, first_byte, .. } => {
175 write!(f, "invalid UTF-8 byte 0x{first_byte:02x} at position {start}")
176 }
177 Self::InvalidMount(msg) => write!(f, "invalid mount: {msg}"),
178 Self::WriteLimitExceeded(limit) => {
179 write!(f, "disk write limit of {} exceeded", format_bytes_pretty(*limit))
180 }
181 Self::MemoryUsageLimitExceeded(limit) => {
182 write!(
183 f,
184 "mount memory usage limit of {} exceeded",
185 format_bytes_pretty(*limit)
186 )
187 }
188 }
189 }
190}
191
192impl Error for MountError {
193 fn source(&self) -> Option<&(dyn Error + 'static)> {
194 match self {
195 Self::Io(err, _) => Some(err),
196 _ => None,
197 }
198 }
199}
200
201fn format_bytes_pretty(bytes: u64) -> String {
208 const KB: u64 = 1_000;
209 const MB: u64 = 1_000_000;
210 const GB: u64 = 1_000_000_000;
211 const TB: u64 = 1_000_000_000_000;
212
213 if bytes < KB {
214 return format!("{bytes} bytes");
215 }
216
217 let (value, unit) = if bytes < MB {
218 (bytes as f64 / KB as f64, "KB")
219 } else if bytes < GB {
220 (bytes as f64 / MB as f64, "MB")
221 } else if bytes < TB {
222 (bytes as f64 / GB as f64, "GB")
223 } else {
224 (bytes as f64 / TB as f64, "TB")
225 };
226
227 let tenths = (value * 10.0).round() % 10.0;
229 if tenths < f64::EPSILON {
230 format!("{value:.0} {unit}")
231 } else {
232 format!("{value:.1} {unit}")
233 }
234}