| 1 | use super::{Entry, EntryKind, Listed, Source, component}; |
| 2 | use crate::fs; |
| 3 | use std::{ |
| 4 | collections::BTreeSet, |
| 5 | io, |
| 6 | path::{Path, PathBuf}, |
| 7 | }; |
| 8 | |
| 9 | /// The notebook file iOS lists as `.Name.icloud` while iCloud Drive keeps it elsewhere. |
| 10 | fn evicted(name: &str) -> Option<&str> { |
| 11 | let real = name.strip_prefix('.')?.strip_suffix(".icloud")?; |
| 12 | let lower = real.to_ascii_lowercase(); |
| 13 | (lower.ends_with(".one") || lower.ends_with(".onetoc2")).then_some(real) |
| 14 | } |
| 15 | |
| 16 | /// Whether the system keeps the file's contents elsewhere until it is read, as macOS 14 and |
| 17 | /// later list a file iCloud Drive evicted (`SF_DATALESS`). |
| 18 | fn dataless(metadata: &fs::Metadata) -> bool { |
| 19 | const SF_DATALESS: u32 = 0x4000_0000; |
| 20 | #[cfg(target_os = "macos")] |
| 21 | let flags = std::os::macos::fs::MetadataExt::st_flags(metadata); |
| 22 | #[cfg(target_os = "ios")] |
| 23 | let flags = std::os::ios::fs::MetadataExt::st_flags(metadata); |
| 24 | #[cfg(not(any(target_os = "macos", target_os = "ios")))] |
| 25 | let flags = { |
| 26 | let _ = metadata; |
| 27 | 0 |
| 28 | }; |
| 29 | flags & SF_DATALESS != 0 |
| 30 | } |
| 31 | |
| 32 | /// Where iOS lists the file at `path` while iCloud Drive keeps it elsewhere. |
| 33 | pub(crate) fn placeholder(path: &Path) -> Option<PathBuf> { |
| 34 | let name = path.file_name()?.to_str()?; |
| 35 | Some(path.with_file_name(format!(".{name}.icloud"))) |
| 36 | } |
| 37 | |
| 38 | /// A canonical local root; symbolic-link entries are not traversed. |
| 39 | pub struct Local { |
| 40 | root: PathBuf, |
| 41 | } |
| 42 | |
| 43 | impl Local { |
| 44 | pub fn open(root: impl AsRef<Path>) -> io::Result<Self> { |
| 45 | let root = fs::canonicalize(root)?; |
| 46 | if !fs::metadata(&root)?.is_dir() { |
| 47 | return Err(io::ErrorKind::NotADirectory.into()); |
| 48 | } |
| 49 | Ok(Self { root }) |
| 50 | } |
| 51 | |
| 52 | fn path(&self, relative: &str) -> io::Result<PathBuf> { |
| 53 | if !relative.is_empty() && !relative.split('/').all(component) { |
| 54 | return Err(io::ErrorKind::InvalidInput.into()); |
| 55 | } |
| 56 | let path = fs::canonicalize(self.root.join(relative))?; |
| 57 | if !path.starts_with(&self.root) { |
| 58 | return Err(io::ErrorKind::PermissionDenied.into()); |
| 59 | } |
| 60 | Ok(path) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | impl Source for Local { |
| 65 | fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<Entry>> { |
| 66 | let mut entries = Vec::new(); |
| 67 | for entry in fs::read_dir(self.path(path)?)? { |
| 68 | let entry = entry?; |
| 69 | if entries.len() == limit { |
| 70 | return Err(io::ErrorKind::FileTooLarge.into()); |
| 71 | } |
| 72 | let name = entry |
| 73 | .file_name() |
| 74 | .into_string() |
| 75 | .map_err(|_| io::ErrorKind::InvalidData)?; |
| 76 | let kind = entry.file_type()?; |
| 77 | let metadata = entry.metadata()?; |
| 78 | entries.push(Entry { |
| 79 | name, |
| 80 | kind: if kind.is_dir() { |
| 81 | EntryKind::Directory |
| 82 | } else if kind.is_file() && dataless(&metadata) { |
| 83 | EntryKind::Evicted |
| 84 | } else if kind.is_file() { |
| 85 | EntryKind::File |
| 86 | } else { |
| 87 | EntryKind::Other |
| 88 | }, |
| 89 | listed: Listed { |
| 90 | size: metadata.len(), |
| 91 | modified: metadata |
| 92 | .modified()? |
| 93 | .duration_since(std::time::UNIX_EPOCH) |
| 94 | .map_or(0, |since| since.as_nanos() as u64), |
| 95 | }, |
| 96 | }); |
| 97 | } |
| 98 | let present: BTreeSet<_> = entries.iter().map(|entry| entry.name.clone()).collect(); |
| 99 | entries.retain_mut(|entry| match evicted(&entry.name).map(str::to_owned) { |
| 100 | Some(real) if present.contains(&real) => false, |
| 101 | Some(real) => { |
| 102 | entry.name = real; |
| 103 | entry.kind = EntryKind::Evicted; |
| 104 | true |
| 105 | } |
| 106 | None => true, |
| 107 | }); |
| 108 | Ok(entries) |
| 109 | } |
| 110 | |
| 111 | fn read(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { |
| 112 | fs::read_file_limited(self.path(path)?, limit) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | #[cfg(feature = "smb")] |
| 117 | /// A share-relative root on an existing blocking SMB connection. |
| 118 | pub struct Smb<'a> { |
| 119 | client: &'a crate::smb::Client, |
| 120 | root: String, |
| 121 | /// Where the sections' replicas are, named by file identity. |
| 122 | copies: Option<PathBuf>, |
| 123 | } |
| 124 | |
| 125 | #[cfg(feature = "smb")] |
| 126 | impl<'a> Smb<'a> { |
| 127 | pub fn new(client: &'a crate::smb::Client, root: &str) -> io::Result<Self> { |
| 128 | let root = root.replace('\\', "/"); |
| 129 | if !root.is_empty() && !root.split('/').all(component) { |
| 130 | return Err(io::ErrorKind::InvalidInput.into()); |
| 131 | } |
| 132 | Ok(Self { |
| 133 | client, |
| 134 | root, |
| 135 | copies: None, |
| 136 | }) |
| 137 | } |
| 138 | |
| 139 | /// Takes a section whose file stands as its replica's base in `folder` from the replica. |
| 140 | pub(crate) fn copies(self, folder: PathBuf) -> Self { |
| 141 | Self { |
| 142 | copies: Some(folder), |
| 143 | ..self |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | fn path(&self, relative: &str) -> io::Result<String> { |
| 148 | if !relative.is_empty() && !relative.split('/').all(component) { |
| 149 | return Err(io::ErrorKind::InvalidInput.into()); |
| 150 | } |
| 151 | Ok(if relative.is_empty() { |
| 152 | self.root.clone() |
| 153 | } else { |
| 154 | super::join(&self.root, relative) |
| 155 | }) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | #[cfg(feature = "smb")] |
| 160 | impl Source for Smb<'_> { |
| 161 | fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<Entry>> { |
| 162 | Ok(self |
| 163 | .client |
| 164 | .read_dir(&self.path(path)?, limit)? |
| 165 | .into_iter() |
| 166 | .map(|entry| Entry { |
| 167 | name: entry.name, |
| 168 | kind: if entry.attributes & 0x400 != 0 { |
| 169 | EntryKind::Other |
| 170 | } else if entry.attributes & 0x10 != 0 { |
| 171 | EntryKind::Directory |
| 172 | } else { |
| 173 | EntryKind::File |
| 174 | }, |
| 175 | listed: Listed { |
| 176 | size: entry.size, |
| 177 | modified: entry.modified, |
| 178 | }, |
| 179 | }) |
| 180 | .collect()) |
| 181 | } |
| 182 | |
| 183 | fn read(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { |
| 184 | self.client.read_storage(&self.path(path)?, limit) |
| 185 | } |
| 186 | |
| 187 | fn read_asset(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { |
| 188 | self.client.read_asset(&self.path(path)?, limit) |
| 189 | } |
| 190 | |
| 191 | /// Reads the file's stamp, then its replica's base if that has the same. |
| 192 | fn copy(&mut self, path: &str, known: [u8; 16]) -> Option<Vec<u8>> { |
| 193 | let replica = crate::session::replica_file(self.copies.as_ref()?, &known); |
| 194 | if fs::metadata(&replica).is_err() { |
| 195 | return None; |
| 196 | } |
| 197 | let stamp = self.client.stamp(&self.path(path).ok()?).ok()?; |
| 198 | let connection = crate::closed(&replica).ok()?; |
| 199 | let base = crate::base::Image::Base; |
| 200 | (crate::base::stamp(&connection, base).ok()?? == stamp) |
| 201 | .then(|| crate::base::read(&connection, base).ok()?) |
| 202 | .flatten() |
| 203 | } |
| 204 | } |