1use std::fs::{self, OpenOptions};
14use std::io::ErrorKind;
15use std::ops::Range;
16use std::path::{Path, PathBuf};
17
18use super::state::{ROOT, child_key};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum NameProblem {
27 Empty,
29 Slash,
31 Nul,
33 Dots,
35 Taken,
37}
38
39impl NameProblem {
40 #[must_use]
42 pub fn message(&self) -> String {
43 let key = match self {
44 Self::Empty => "quvyta.file-manager.name-empty",
45 Self::Slash => "quvyta.file-manager.name-slash",
46 Self::Nul => "quvyta.file-manager.name-nul",
47 Self::Dots => "quvyta.file-manager.name-dots",
48 Self::Taken => "quvyta.file-manager.name-taken",
49 };
50 crate::t!(key)
51 }
52}
53
54pub fn check_name<'a>(
62 name: &str,
63 mut siblings: impl Iterator<Item = &'a str>,
64 current: Option<&str>,
65) -> Result<(), NameProblem> {
66 if name.trim().is_empty() {
67 return Err(NameProblem::Empty);
68 }
69 if name.contains('/') {
70 return Err(NameProblem::Slash);
71 }
72 if name.contains('\0') {
73 return Err(NameProblem::Nul);
74 }
75 if name == "." || name == ".." {
76 return Err(NameProblem::Dots);
77 }
78 if current != Some(name) && siblings.any(|sibling| sibling == name) {
79 return Err(NameProblem::Taken);
80 }
81 Ok(())
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum FileError {
91 Name(NameProblem),
93 Outside,
95 IntoItself,
97 Taken(String),
99 CrossDevice,
101 Denied,
103 NotReadable,
105 Missing,
107 NoTrash,
109 NoRoom,
111 Stopped,
113 System(String),
115}
116
117impl FileError {
118 #[must_use]
120 pub fn message(&self) -> String {
121 match self {
122 Self::Name(problem) => problem.message(),
123 Self::Outside => crate::t!("quvyta.file-manager.outside"),
124 Self::IntoItself => crate::t!("quvyta.file-manager.into-itself"),
125 Self::Taken(name) => crate::t!("quvyta.file-manager.taken", name = name.as_str()),
126 Self::CrossDevice => crate::t!("quvyta.file-manager.cross-device"),
127 Self::Denied => crate::t!("quvyta.file-manager.denied"),
128 Self::NotReadable => crate::t!("quvyta.file-manager.not-readable"),
129 Self::Missing => crate::t!("quvyta.file-manager.missing"),
130 Self::NoTrash => crate::t!("quvyta.file-manager.no-trash"),
131 Self::NoRoom => crate::t!("quvyta.file-manager.no-room"),
132 Self::Stopped => crate::t!("quvyta.file-manager.stopped"),
133 Self::System(said) => said.clone(),
134 }
135 }
136}
137
138impl From<std::io::Error> for FileError {
139 fn from(error: std::io::Error) -> Self {
140 match error.kind() {
141 ErrorKind::CrossesDevices => Self::CrossDevice,
143 ErrorKind::PermissionDenied => Self::Denied,
144 ErrorKind::NotFound => Self::Missing,
145 ErrorKind::StorageFull => Self::NoRoom,
146 _ => Self::System(error.to_string()),
147 }
148 }
149}
150
151pub(super) fn read_error(error: std::io::Error) -> FileError {
156 if error.kind() == ErrorKind::PermissionDenied { FileError::NotReadable } else { error.into() }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
163#[non_exhaustive]
164pub enum FileChange {
165 Created(String),
167 Moved(String, String),
169 Deleted(String),
171 Copied(String),
173 Trashed(String),
175}
176
177#[must_use]
179pub fn parent_key(key: &str) -> &str {
180 key.rsplit_once('/').map_or(ROOT, |(parent, _)| parent)
181}
182
183#[must_use]
185pub fn name_of(key: &str) -> &str {
186 key.rsplit_once('/').map_or(key, |(_, name)| name)
187}
188
189#[must_use]
195pub fn stem(name: &str, folder: bool) -> Range<usize> {
196 let length = name.chars().count();
197 if folder {
198 return 0..length;
199 }
200 match name.chars().rev().position(|c| c == '.').map(|from_end| length - 1 - from_end) {
201 Some(dot) if dot > 0 => 0..dot,
202 _ => 0..length,
203 }
204}
205
206#[must_use]
208pub fn is_within(key: &str, folder: &str) -> bool {
209 key == folder || key.strip_prefix(folder).is_some_and(|rest| rest.starts_with('/'))
210}
211
212#[must_use]
219pub fn is_inside(key: &str) -> bool {
220 !key.is_empty() && !key.contains('\0') && key.split('/').all(|part| !part.is_empty() && part != "." && part != "..")
221}
222
223fn entry_path(root: &Path, key: &str, confined: bool) -> Result<PathBuf, FileError> {
228 let parts: Vec<&str> = key.split('/').collect();
229 let mut path = root.to_path_buf();
230 for (index, part) in parts.iter().enumerate() {
231 if part.is_empty() || *part == "." || *part == ".." || part.contains('\0') {
232 return Err(FileError::Outside);
233 }
234 path.push(part);
235 if confined && index + 1 < parts.len() {
236 real_folder(&path)?;
237 }
238 }
239 Ok(path)
240}
241
242fn folder_path(root: &Path, key: &str, confined: bool) -> Result<PathBuf, FileError> {
244 if key == ROOT {
245 return Ok(root.to_path_buf());
246 }
247 let path = entry_path(root, key, confined)?;
248 if confined {
249 real_folder(&path)?;
250 }
251 Ok(path)
252}
253
254fn real_folder(path: &Path) -> Result<(), FileError> {
256 let meta = fs::symlink_metadata(path)?;
257 if meta.file_type().is_symlink() || !meta.is_dir() {
258 return Err(FileError::Outside);
259 }
260 Ok(())
261}
262
263fn occupied(path: &Path) -> bool {
265 fs::symlink_metadata(path).is_ok()
266}
267
268fn usable(name: &str) -> Result<(), FileError> {
271 check_name(name, std::iter::empty(), None).map_err(FileError::Name)
272}
273
274pub fn create_file(root: &Path, folder: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
281 usable(name)?;
282 let path = folder_path(root, folder, confined)?.join(name);
283 OpenOptions::new().write(true).create_new(true).open(&path).map_err(|error| taken_or(error, name))?;
286 Ok(FileChange::Created(child_key(folder, name)))
287}
288
289pub fn create_folder(root: &Path, folder: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
295 usable(name)?;
296 let path = folder_path(root, folder, confined)?.join(name);
297 fs::create_dir(&path).map_err(|error| taken_or(error, name))?;
298 Ok(FileChange::Created(child_key(folder, name)))
299}
300
301pub fn rename(root: &Path, key: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
308 usable(name)?;
309 let from = entry_path(root, key, confined)?;
310 let folder = parent_key(key);
311 if name_of(key) == name {
312 return Ok(FileChange::Moved(key.to_owned(), key.to_owned()));
313 }
314 let to = folder_path(root, folder, confined)?.join(name);
315 if occupied(&to) {
316 return Err(FileError::Taken(name.to_owned()));
317 }
318 fs::rename(&from, &to)?;
319 Ok(FileChange::Moved(key.to_owned(), child_key(folder, name)))
320}
321
322pub fn move_into(root: &Path, key: &str, into: &str, confined: bool) -> Result<FileChange, FileError> {
332 if is_within(into, key) {
333 return Err(FileError::IntoItself);
334 }
335 let from = entry_path(root, key, confined)?;
336 let name = name_of(key);
337 if parent_key(key) == into {
338 return Ok(FileChange::Moved(key.to_owned(), key.to_owned()));
339 }
340 let to = folder_path(root, into, confined)?.join(name);
341 if occupied(&to) {
342 return Err(FileError::Taken(name.to_owned()));
343 }
344 fs::rename(&from, &to)?;
345 Ok(FileChange::Moved(key.to_owned(), child_key(into, name)))
346}
347
348pub fn delete(root: &Path, key: &str, confined: bool) -> Result<FileChange, FileError> {
355 let path = entry_path(root, key, confined)?;
356 let meta = fs::symlink_metadata(&path)?;
357 if meta.is_dir() {
360 fs::remove_dir_all(&path)?;
361 } else {
362 fs::remove_file(&path)?;
363 }
364 Ok(FileChange::Deleted(key.to_owned()))
365}
366
367pub(super) fn to_trash(trash: &Path, root: &Path, key: &str, confined: bool) -> Result<FileChange, FileError> {
374 let path = entry_path(root, key, confined)?;
375 fs::symlink_metadata(&path)?;
377 super::trash::move_to_trash(trash, &path)?;
378 Ok(FileChange::Trashed(key.to_owned()))
379}
380
381fn taken_or(error: std::io::Error, name: &str) -> FileError {
383 if error.kind() == ErrorKind::AlreadyExists { FileError::Taken(name.to_owned()) } else { error.into() }
384}
385
386pub(super) struct Watch<'a> {
391 pub(super) progress: &'a dyn Fn(u64, u64),
393 pub(super) stopped: &'a dyn Fn() -> bool,
395}
396
397impl Watch<'_> {
398 pub(super) fn none() -> Self {
400 Self { progress: &|_, _| {}, stopped: &|| false }
401 }
402}
403
404const BLOCK: usize = 256 * 1024;
407
408fn weight(path: &Path) -> u64 {
411 let Ok(meta) = fs::symlink_metadata(path) else { return 0 };
412 if !meta.is_dir() {
413 return meta.len();
414 }
415 let Ok(entries) = fs::read_dir(path) else { return 0 };
416 entries.flatten().map(|entry| weight(&entry.path())).sum()
417}
418
419pub fn copy_into(root: &Path, key: &str, into: &str, confined: bool) -> Result<FileChange, FileError> {
431 copy_watched(root, key, into, confined, &Watch::none())
432}
433
434pub(super) fn copy_watched(
436 root: &Path,
437 key: &str,
438 into: &str,
439 confined: bool,
440 watch: &Watch<'_>,
441) -> Result<FileChange, FileError> {
442 if is_within(into, key) {
443 return Err(FileError::IntoItself);
444 }
445 let from = entry_path(root, key, confined)?;
446 let name = name_of(key);
447 let to = folder_path(root, into, confined)?.join(name);
448 if occupied(&to) {
449 return Err(FileError::Taken(name.to_owned()));
450 }
451 let total = weight(&from);
452 let mut written = 0;
453 (watch.progress)(0, total);
454 match copy_tree(&from, &to, total, &mut written, watch) {
455 Ok(()) => Ok(FileChange::Copied(child_key(into, name))),
456 Err(problem) => {
457 let _ = fs::symlink_metadata(&to)
463 .map(|meta| if meta.is_dir() { fs::remove_dir_all(&to) } else { fs::remove_file(&to) });
464 Err(problem)
465 }
466 }
467}
468
469fn copy_tree(from: &Path, to: &Path, total: u64, written: &mut u64, watch: &Watch<'_>) -> Result<(), FileError> {
472 if (watch.stopped)() {
473 return Err(FileError::Stopped);
474 }
475 let meta = fs::symlink_metadata(from)?;
476 if meta.file_type().is_symlink() {
477 let target = fs::read_link(from)?;
480 std::os::unix::fs::symlink(target, to)?;
481 return Ok(());
482 }
483 if !meta.is_dir() {
484 copy_file(from, to, total, written, watch)?;
485 return Ok(());
486 }
487 fs::create_dir(to)?;
488 let mut entries: Vec<PathBuf> = fs::read_dir(from)?.flatten().map(|entry| entry.path()).collect();
489 entries.sort();
490 for entry in entries {
491 let Some(name) = entry.file_name() else { continue };
492 copy_tree(&entry, &to.join(name), total, written, watch)?;
493 }
494 fs::set_permissions(to, meta.permissions())?;
497 Ok(())
498}
499
500fn copy_file(from: &Path, to: &Path, total: u64, written: &mut u64, watch: &Watch<'_>) -> Result<(), FileError> {
503 use std::io::{Read, Write};
504
505 let mut source = fs::File::open(from)?;
506 let mut target = OpenOptions::new().write(true).create_new(true).open(to)?;
507 let mut block = vec![0u8; BLOCK];
508 loop {
509 if (watch.stopped)() {
510 return Err(FileError::Stopped);
511 }
512 let read = source.read(&mut block)?;
513 if read == 0 {
514 break;
515 }
516 target.write_all(&block[..read])?;
517 *written += read as u64;
518 (watch.progress)(*written, total);
519 }
520 target.flush()?;
521 fs::set_permissions(to, fs::metadata(from)?.permissions())?;
523 Ok(())
524}