1use crate::{
13 descriptor::{hex, valid_hash},
14 filesystem as io, ObjectDescriptor, TransferControl, TransferError as E, TransferLimits,
15 TransferResult,
16};
17use fs2::FileExt;
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::{
21 fs::{self, File},
22 io::{Read, Write},
23 path::{Path, PathBuf},
24 time::{Duration, SystemTime, UNIX_EPOCH},
25};
26#[derive(Debug)]
30pub struct TransferStore {
31 root: PathBuf,
32 limits: TransferLimits,
33 _lock: File,
34 #[cfg(windows)]
35 _root_guard: appcore_security::PrivateDirectoryGuard,
36 used: u64,
37 files: usize,
38}
39#[derive(Serialize)]
40struct Metadata {
41 format_version: u16,
42 created_ms: u64,
43 descriptor: ObjectDescriptor,
44}
45#[derive(Deserialize)]
46#[serde(deny_unknown_fields)]
47struct MetadataWire {
48 format_version: u16,
49 created_ms: u64,
50 descriptor: crate::descriptor::DescriptorWire,
51}
52#[derive(Debug, Clone)]
54pub struct VerifiedObject {
55 path: PathBuf,
56 descriptor: ObjectDescriptor,
57}
58impl VerifiedObject {
59 pub fn path(&self) -> &Path {
61 &self.path
62 }
63 pub fn descriptor(&self) -> &ObjectDescriptor {
65 &self.descriptor
66 }
67 pub fn open(&self) -> TransferResult<File> {
69 verify_complete(&self.path, &self.descriptor)?;
70 io::open(&self.path)
71 }
72}
73pub struct TransferSession<'a> {
75 store: &'a mut TransferStore,
76 dir: PathBuf,
77 descriptor: ObjectDescriptor,
78 complete: Vec<bool>,
79 #[cfg(windows)]
80 _directory_guard: appcore_security::PrivateDirectoryGuard,
81}
82impl TransferStore {
83 pub fn open(root: impl Into<PathBuf>, limits: TransferLimits) -> TransferResult<Self> {
86 limits.validate()?;
87 let root = root.into();
88 io::validate_root(&root)?;
89 #[cfg(windows)]
90 let root_guard =
91 appcore_security::create_update_private_directory(&root).map_err(|_| E::UnsafePath)?;
92 let path = root.join(".lock");
93 let lock = if path.try_exists().map_err(|_| E::Storage)? {
94 io::open(&path)?
95 } else {
96 io::exclusive(&path)?
97 };
98 lock.try_lock_exclusive().map_err(|_| E::Busy)?;
99 let mut value = Self {
100 root,
101 limits,
102 _lock: lock,
103 #[cfg(windows)]
104 _root_guard: root_guard,
105 used: 0,
106 files: 0,
107 };
108 value.recount()?;
109 Ok(value)
110 }
111 pub fn begin(&mut self, descriptor: ObjectDescriptor) -> TransferResult<TransferSession<'_>> {
115 descriptor.validate(self.limits)?;
116 let dir = self.root.join(descriptor.object_hash());
117 if !dir.try_exists().map_err(|_| E::Storage)? {
118 self.reserve(0, 1)?;
119 io::make_directory(&dir)?;
120 self.recount()?;
121 }
122 io::private_directory(&dir)?;
123 #[cfg(windows)]
124 let directory_guard =
125 appcore_security::create_update_private_directory(&dir).map_err(|_| E::UnsafePath)?;
126 let metadata_path = dir.join("descriptor.json");
127 if metadata_path.try_exists().map_err(|_| E::Storage)? {
128 let metadata = read_metadata(&metadata_path, self.limits)?;
129 if metadata.descriptor != descriptor {
130 return Err(E::Integrity);
131 }
132 } else {
133 for entry in fs::read_dir(&dir).map_err(|_| E::Storage)? {
134 let entry = entry.map_err(|_| E::Storage)?;
135 io::regular(&entry.path())?;
136 if entry.file_name() != "descriptor.tmp" {
137 return Err(E::Integrity);
138 }
139 fs::remove_file(entry.path()).map_err(|_| E::Storage)?;
140 }
141 self.recount()?;
142 let metadata = Metadata {
143 format_version: 1,
144 created_ms: now_ms()?,
145 descriptor: descriptor.clone(),
146 };
147 let bytes = serde_json::to_vec(&metadata).map_err(|_| E::Storage)?;
148 self.reserve(bytes.len() as u64, 2)?;
149 io::write(&metadata_path, &bytes)?;
150 self.recount()?;
151 }
152 cleanup_temporaries(&dir, &descriptor)?;
153 self.recount()?;
154 let mut complete = vec![false; descriptor.block_count()];
155 let final_path = dir.join("complete.bin");
156 if final_path.try_exists().map_err(|_| E::Storage)? {
157 verify_complete(&final_path, &descriptor)?;
158 complete.fill(true);
159 } else {
160 for (index, done) in complete.iter_mut().enumerate() {
161 let path = block_path(&dir, index);
162 if path.try_exists().map_err(|_| E::Storage)? {
163 let bytes = io::read(&path, descriptor.block_len(index)? as u64)?;
164 descriptor.verify_block(index, &bytes)?;
165 *done = true;
166 }
167 }
168 }
169 Ok(TransferSession {
170 store: self,
171 dir,
172 descriptor,
173 complete,
174 #[cfg(windows)]
175 _directory_guard: directory_guard,
176 })
177 }
178 pub fn prune_expired(&mut self, ttl: Duration) -> TransferResult<usize> {
181 if ttl.is_zero() || ttl > Duration::from_secs(365 * 86400) {
182 return Err(E::InvalidContract);
183 }
184 let now = now_ms()?;
185 let mut removed = 0;
186 for entry in fs::read_dir(&self.root).map_err(|_| E::Storage)? {
187 let entry = entry.map_err(|_| E::Storage)?;
188 if entry.file_name() == ".lock" {
189 continue;
190 }
191 let dir = entry.path();
192 io::private_directory(&dir)?;
193 let m = read_metadata(&dir.join("descriptor.json"), self.limits)?;
194 if dir.file_name().and_then(|name| name.to_str()) != Some(m.descriptor.object_hash()) {
195 return Err(E::Integrity);
196 }
197 validate_inventory(&dir, &m.descriptor)?;
198 if !dir
199 .join("complete.bin")
200 .try_exists()
201 .map_err(|_| E::Storage)?
202 && now.saturating_sub(m.created_ms) >= ttl.as_millis() as u64
203 {
204 fs::remove_dir_all(&dir).map_err(|_| E::Storage)?;
205 io::sync_parent(&dir)?;
206 removed += 1;
207 }
208 }
209 self.recount()?;
210 Ok(removed)
211 }
212 pub fn purge_partial(&mut self, descriptor: &ObjectDescriptor) -> TransferResult<()> {
215 descriptor.validate(self.limits)?;
216 let dir = self.root.join(descriptor.object_hash());
217 io::private_directory(&dir)?;
218 if read_metadata(&dir.join("descriptor.json"), self.limits)?.descriptor != *descriptor {
219 return Err(E::Integrity);
220 }
221 if dir
222 .join("complete.bin")
223 .try_exists()
224 .map_err(|_| E::Storage)?
225 {
226 return Err(E::InvalidContract);
227 }
228 validate_inventory(&dir, descriptor)?;
229 fs::remove_dir_all(&dir).map_err(|_| E::Storage)?;
230 io::sync_parent(&dir)?;
231 self.recount()
232 }
233 fn reserve(&self, bytes: u64, files: usize) -> TransferResult<()> {
234 if self
235 .used
236 .checked_add(bytes)
237 .is_none_or(|n| n > self.limits.max_store_bytes)
238 || self
239 .files
240 .checked_add(files)
241 .is_none_or(|n| n > self.limits.max_files)
242 {
243 return Err(E::Limit);
244 }
245 Ok(())
246 }
247 fn recount(&mut self) -> TransferResult<()> {
248 let mut bytes = 0u64;
249 let mut count = 0usize;
250 for entry in fs::read_dir(&self.root).map_err(|_| E::Storage)? {
251 let entry = entry.map_err(|_| E::Storage)?;
252 count += 1;
253 if count > self.limits.max_files {
254 return Err(E::Limit);
255 }
256 if entry.file_name() == ".lock" {
257 bytes = bytes
258 .checked_add(io::regular(&entry.path())?.len())
259 .ok_or(E::Limit)?;
260 continue;
261 }
262 if !entry.file_name().to_str().is_some_and(valid_hash) {
263 return Err(E::UnsafePath);
264 }
265 io::private_directory(&entry.path())?;
266 for file in fs::read_dir(entry.path()).map_err(|_| E::Storage)? {
267 let file = file.map_err(|_| E::Storage)?;
268 count += 1;
269 if count > self.limits.max_files {
270 return Err(E::Limit);
271 }
272 bytes = bytes
273 .checked_add(io::regular(&file.path())?.len())
274 .ok_or(E::Limit)?;
275 if bytes > self.limits.max_store_bytes {
276 return Err(E::Limit);
277 }
278 }
279 }
280 if bytes > self.limits.max_store_bytes {
281 return Err(E::Limit);
282 }
283 self.used = bytes;
284 self.files = count;
285 Ok(())
286 }
287}
288impl TransferSession<'_> {
289 pub fn descriptor(&self) -> &ObjectDescriptor {
291 &self.descriptor
292 }
293 pub fn missing(&self) -> Vec<usize> {
295 self.complete
296 .iter()
297 .enumerate()
298 .filter_map(|(i, done)| (!done).then_some(i))
299 .collect()
300 }
301 pub fn accept(&mut self, index: usize, bytes: &[u8]) -> TransferResult<()> {
304 self.descriptor.verify_block(index, bytes)?;
305 if self.complete[index] {
306 return Ok(());
307 }
308 self.store.reserve(bytes.len() as u64, 2)?;
309 let result = io::write(&block_path(&self.dir, index), bytes);
310 self.store.recount()?;
311 result?;
312 self.complete[index] = true;
313 Ok(())
314 }
315 pub fn finish(self) -> TransferResult<VerifiedObject> {
319 self.finish_inner(None)
320 }
321 pub fn finish_with_control(self, control: &TransferControl) -> TransferResult<VerifiedObject> {
324 self.finish_inner(Some(control))
325 }
326 fn finish_inner(self, control: Option<&TransferControl>) -> TransferResult<VerifiedObject> {
327 checkpoint(control)?;
328 if self.complete.iter().any(|done| !*done) {
329 return Err(E::Integrity);
330 }
331 let path = self.dir.join("complete.bin");
332 if !path.try_exists().map_err(|_| E::Storage)? {
333 self.store.reserve(self.descriptor.total_bytes(), 1)?;
334 let result = io::atomic(&path, |file| {
335 assemble(file, &self.dir, &self.descriptor, control)
336 });
337 self.store.recount()?;
338 result?;
339 }
340 verify_complete_controlled(&path, &self.descriptor, control)?;
341 for index in 0..self.descriptor.block_count() {
342 let block = block_path(&self.dir, index);
343 if block.try_exists().map_err(|_| E::Storage)? {
344 io::regular(&block)?;
345 fs::remove_file(block).map_err(|_| E::Storage)?;
346 }
347 }
348 io::sync_parent(&path)?;
349 self.store.recount()?;
350 Ok(VerifiedObject {
351 path,
352 descriptor: self.descriptor,
353 })
354 }
355}
356fn assemble(
357 file: &mut File,
358 dir: &Path,
359 descriptor: &ObjectDescriptor,
360 control: Option<&TransferControl>,
361) -> TransferResult<()> {
362 let mut full = Sha256::new();
363 for index in 0..descriptor.block_count() {
364 checkpoint(control)?;
365 let bytes = io::read(&block_path(dir, index), descriptor.block_len(index)? as u64)?;
366 descriptor.verify_block(index, &bytes)?;
367 file.write_all(&bytes).map_err(|_| E::Storage)?;
368 full.update(&bytes);
369 }
370 checkpoint(control)?;
371 if hex(&full.finalize()) != descriptor.object_hash() {
372 return Err(E::Integrity);
373 }
374 Ok(())
375}
376fn checkpoint(control: Option<&TransferControl>) -> TransferResult<()> {
377 control.map_or(Ok(()), TransferControl::checkpoint)
378}
379fn verify_complete(path: &Path, descriptor: &ObjectDescriptor) -> TransferResult<()> {
380 verify_complete_controlled(path, descriptor, None)
381}
382fn verify_complete_controlled(
383 path: &Path,
384 descriptor: &ObjectDescriptor,
385 control: Option<&TransferControl>,
386) -> TransferResult<()> {
387 let mut file = io::open(path)?;
388 if file.metadata().map_err(|_| E::Storage)?.len() != descriptor.total_bytes() {
389 return Err(E::Integrity);
390 }
391 let mut hasher = Sha256::new();
392 let mut buffer = [0; 64 * 1024];
393 loop {
394 checkpoint(control)?;
395 let n = file.read(&mut buffer).map_err(|_| E::Storage)?;
396 if n == 0 {
397 break;
398 }
399 hasher.update(&buffer[..n]);
400 }
401 if hex(&hasher.finalize()) != descriptor.object_hash() {
402 return Err(E::Integrity);
403 }
404 Ok(())
405}
406fn block_path(dir: &Path, index: usize) -> PathBuf {
407 dir.join(format!("{index:04}.block"))
408}
409fn read_metadata(path: &Path, limits: TransferLimits) -> TransferResult<Metadata> {
410 let bytes = io::read(path, 1024 * 1024)?;
411 let wire: MetadataWire = serde_json::from_slice(&bytes).map_err(|_| E::Integrity)?;
412 if wire.format_version != 1 {
413 return Err(E::UnsupportedVersion);
414 }
415 let m = Metadata {
416 format_version: wire.format_version,
417 created_ms: wire.created_ms,
418 descriptor: ObjectDescriptor::try_from(wire.descriptor)?,
419 };
420 if m.format_version != 1 {
421 return Err(E::UnsupportedVersion);
422 }
423 m.descriptor.validate(limits)?;
424 Ok(m)
425}
426fn validate_inventory(dir: &Path, descriptor: &ObjectDescriptor) -> TransferResult<()> {
427 for entry in fs::read_dir(dir).map_err(|_| E::Storage)? {
428 let entry = entry.map_err(|_| E::Storage)?;
429 io::regular(&entry.path())?;
430 let name = entry.file_name();
431 let name = name.to_str().ok_or(E::UnsafePath)?;
432 let valid = name == "descriptor.json"
433 || name == "descriptor.tmp"
434 || name == "complete.bin"
435 || name == "complete.tmp"
436 || (0..descriptor.block_count())
437 .any(|i| name == format!("{i:04}.block") || name == format!("{i:04}.tmp"));
438 if !valid {
439 return Err(E::UnsafePath);
440 }
441 }
442 Ok(())
443}
444fn cleanup_temporaries(dir: &Path, descriptor: &ObjectDescriptor) -> TransferResult<()> {
445 validate_inventory(dir, descriptor)?;
446 for entry in fs::read_dir(dir).map_err(|_| E::Storage)? {
447 let entry = entry.map_err(|_| E::Storage)?;
448 if entry.path().extension().is_some_and(|e| e == "tmp") {
449 fs::remove_file(entry.path()).map_err(|_| E::Storage)?;
450 }
451 }
452 io::sync_parent(&dir.join("descriptor.json"))
453}
454fn now_ms() -> TransferResult<u64> {
455 u64::try_from(
456 SystemTime::now()
457 .duration_since(UNIX_EPOCH)
458 .map_err(|_| E::Storage)?
459 .as_millis(),
460 )
461 .map_err(|_| E::Storage)
462}