1use super::{Entry, EntryKind, Listed, Source, component};
2use crate::fs;
3use 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.
10fn 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`).
18fn 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.
33pub(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.
39pub struct Local {
40 root: PathBuf,
41}
42
43impl 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
64impl 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.
118pub 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")]
126impl<'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")]
160impl 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}