##// END OF EJS Templates
rust-vfs: add docstrings to all VFS methods on the trait
Raphaël Gomès -
r53079:85bff84f default
parent child Browse files
Show More
@@ -1,1101 +1,1131
1 1 use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
2 2 use crate::exit_codes;
3 3 use crate::fncache::FnCache;
4 4 use crate::revlog::path_encode::path_encode;
5 5 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
6 6 use dyn_clone::DynClone;
7 7 use format_bytes::format_bytes;
8 8 use memmap2::{Mmap, MmapOptions};
9 9 use rand::distributions::{Alphanumeric, DistString};
10 10 use std::fs::{File, Metadata, OpenOptions};
11 11 use std::io::{ErrorKind, Read, Seek, Write};
12 12 use std::os::fd::AsRawFd;
13 13 use std::os::unix::fs::{MetadataExt, PermissionsExt};
14 14 use std::path::{Path, PathBuf};
15 15 #[cfg(test)]
16 16 use std::sync::atomic::AtomicUsize;
17 17 #[cfg(test)]
18 18 use std::sync::atomic::Ordering;
19 19 use std::sync::OnceLock;
20 20
21 21 /// Filesystem access abstraction for the contents of a given "base" diretory
22 22 #[derive(Clone)]
23 23 pub struct VfsImpl {
24 24 pub(crate) base: PathBuf,
25 25 pub readonly: bool,
26 26 pub mode: Option<u32>,
27 27 }
28 28
29 29 struct FileNotFound(std::io::Error, PathBuf);
30 30
31 31 /// Store the umask for the whole process since it's expensive to get.
32 32 static UMASK: OnceLock<u32> = OnceLock::new();
33 33
34 34 fn get_umask() -> u32 {
35 35 *UMASK.get_or_init(|| unsafe {
36 36 // TODO is there any way of getting the umask without temporarily
37 37 // setting it? Doesn't this affect all threads in this tiny window?
38 38 let mask = libc::umask(0);
39 39 libc::umask(mask);
40 40 mask & 0o777
41 41 })
42 42 }
43 43
44 44 /// Return the (unix) mode with which we will create/fix files
45 45 fn get_mode(base: impl AsRef<Path>) -> Option<u32> {
46 46 match base.as_ref().metadata() {
47 47 Ok(meta) => {
48 48 // files in .hg/ will be created using this mode
49 49 let mode = meta.mode();
50 50 // avoid some useless chmods
51 51 if (0o777 & !get_umask()) == (0o777 & mode) {
52 52 None
53 53 } else {
54 54 Some(mode)
55 55 }
56 56 }
57 57 Err(_) => None,
58 58 }
59 59 }
60 60
61 61 impl VfsImpl {
62 62 pub fn new(base: PathBuf, readonly: bool) -> Self {
63 63 let mode = get_mode(&base);
64 64 Self {
65 65 base,
66 66 readonly,
67 67 mode,
68 68 }
69 69 }
70 70
71 71 // XXX these methods are probably redundant with VFS trait?
72 72
73 73 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
74 74 self.base.join(relative_path)
75 75 }
76 76
77 77 pub fn symlink_metadata(
78 78 &self,
79 79 relative_path: impl AsRef<Path>,
80 80 ) -> Result<std::fs::Metadata, HgError> {
81 81 let path = self.join(relative_path);
82 82 std::fs::symlink_metadata(&path).when_reading_file(&path)
83 83 }
84 84
85 85 pub fn read_link(
86 86 &self,
87 87 relative_path: impl AsRef<Path>,
88 88 ) -> Result<PathBuf, HgError> {
89 89 let path = self.join(relative_path);
90 90 std::fs::read_link(&path).when_reading_file(&path)
91 91 }
92 92
93 93 pub fn read(
94 94 &self,
95 95 relative_path: impl AsRef<Path>,
96 96 ) -> Result<Vec<u8>, HgError> {
97 97 let path = self.join(relative_path);
98 98 std::fs::read(&path).when_reading_file(&path)
99 99 }
100 100
101 101 /// Returns `Ok(None)` if the file does not exist.
102 102 pub fn try_read(
103 103 &self,
104 104 relative_path: impl AsRef<Path>,
105 105 ) -> Result<Option<Vec<u8>>, HgError> {
106 106 match self.read(relative_path) {
107 107 Err(e) => match &e {
108 108 HgError::IoError { error, .. } => match error.kind() {
109 109 ErrorKind::NotFound => Ok(None),
110 110 _ => Err(e),
111 111 },
112 112 _ => Err(e),
113 113 },
114 114 Ok(v) => Ok(Some(v)),
115 115 }
116 116 }
117 117
118 118 fn mmap_open_gen(
119 119 &self,
120 120 relative_path: impl AsRef<Path>,
121 121 ) -> Result<Result<Mmap, FileNotFound>, HgError> {
122 122 let path = self.join(relative_path);
123 123 let file = match std::fs::File::open(&path) {
124 124 Err(err) => {
125 125 if let ErrorKind::NotFound = err.kind() {
126 126 return Ok(Err(FileNotFound(err, path)));
127 127 };
128 128 return (Err(err)).when_reading_file(&path);
129 129 }
130 130 Ok(file) => file,
131 131 };
132 132 // Safety is "enforced" by locks and assuming other processes are
133 133 // well-behaved. If any misbehaving or malicious process does touch
134 134 // the index, it could lead to corruption. This is inherent
135 135 // to file-based `mmap`, though some platforms have some ways of
136 136 // mitigating.
137 137 // TODO linux: set the immutable flag with `chattr(1)`?
138 138 let mmap = unsafe { MmapOptions::new().map(&file) }
139 139 .when_reading_file(&path)?;
140 140 Ok(Ok(mmap))
141 141 }
142 142
143 143 pub fn mmap_open_opt(
144 144 &self,
145 145 relative_path: impl AsRef<Path>,
146 146 ) -> Result<Option<Mmap>, HgError> {
147 147 self.mmap_open_gen(relative_path).map(|res| res.ok())
148 148 }
149 149
150 150 pub fn mmap_open(
151 151 &self,
152 152 relative_path: impl AsRef<Path>,
153 153 ) -> Result<Mmap, HgError> {
154 154 match self.mmap_open_gen(relative_path)? {
155 155 Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path),
156 156 Ok(res) => Ok(res),
157 157 }
158 158 }
159 159
160 160 #[cfg(unix)]
161 161 pub fn create_symlink(
162 162 &self,
163 163 relative_link_path: impl AsRef<Path>,
164 164 target_path: impl AsRef<Path>,
165 165 ) -> Result<(), HgError> {
166 166 let link_path = self.join(relative_link_path);
167 167 std::os::unix::fs::symlink(target_path, &link_path)
168 168 .when_writing_file(&link_path)
169 169 }
170 170
171 171 /// Write `contents` into a temporary file, then rename to `relative_path`.
172 172 /// This makes writing to a file "atomic": a reader opening that path will
173 173 /// see either the previous contents of the file or the complete new
174 174 /// content, never a partial write.
175 175 pub fn atomic_write(
176 176 &self,
177 177 relative_path: impl AsRef<Path>,
178 178 contents: &[u8],
179 179 ) -> Result<(), HgError> {
180 180 let mut tmp = tempfile::NamedTempFile::new_in(&self.base)
181 181 .when_writing_file(&self.base)?;
182 182 tmp.write_all(contents)
183 183 .and_then(|()| tmp.flush())
184 184 .when_writing_file(tmp.path())?;
185 185 let path = self.join(relative_path);
186 186 tmp.persist(&path)
187 187 .map_err(|e| e.error)
188 188 .when_writing_file(&path)?;
189 189 Ok(())
190 190 }
191 191 }
192 192
193 193 fn fs_metadata(
194 194 path: impl AsRef<Path>,
195 195 ) -> Result<Option<std::fs::Metadata>, HgError> {
196 196 let path = path.as_ref();
197 197 match path.metadata() {
198 198 Ok(meta) => Ok(Some(meta)),
199 199 Err(error) => match error.kind() {
200 200 // TODO: when we require a Rust version where `NotADirectory` is
201 201 // stable, invert this logic and return None for it and `NotFound`
202 202 // and propagate any other error.
203 203 ErrorKind::PermissionDenied => Err(error).with_context(|| {
204 204 IoErrorContext::ReadingMetadata(path.to_owned())
205 205 }),
206 206 _ => Ok(None),
207 207 },
208 208 }
209 209 }
210 210
211 211 /// Abstraction over the files handled by a [`Vfs`].
212 212 #[derive(Debug)]
213 213 pub enum VfsFile {
214 214 Atomic(AtomicFile),
215 215
216 216 Normal {
217 217 file: File,
218 218 path: PathBuf,
219 219 /// If `Some`, check (and maybe fix) this file's timestamp ambiguity.
220 220 /// See [`is_filetime_ambiguous`].
221 221 check_ambig: Option<Metadata>,
222 222 },
223 223 }
224 224
225 225 impl VfsFile {
226 226 pub fn normal(file: File, path: PathBuf) -> Self {
227 227 Self::Normal {
228 228 file,
229 229 check_ambig: None,
230 230 path,
231 231 }
232 232 }
233 233 pub fn normal_check_ambig(
234 234 file: File,
235 235 path: PathBuf,
236 236 ) -> Result<Self, HgError> {
237 237 Ok(Self::Normal {
238 238 file,
239 239 check_ambig: Some(path.metadata().when_reading_file(&path)?),
240 240 path,
241 241 })
242 242 }
243 243 pub fn try_clone(&self) -> Result<VfsFile, HgError> {
244 244 Ok(match self {
245 245 VfsFile::Atomic(AtomicFile {
246 246 fp,
247 247 temp_path,
248 248 check_ambig,
249 249 target_name,
250 250 is_open,
251 251 }) => Self::Atomic(AtomicFile {
252 252 fp: fp.try_clone().when_reading_file(temp_path)?,
253 253 temp_path: temp_path.clone(),
254 254 check_ambig: *check_ambig,
255 255 target_name: target_name.clone(),
256 256 is_open: *is_open,
257 257 }),
258 258 VfsFile::Normal {
259 259 file,
260 260 check_ambig,
261 261 path,
262 262 } => Self::Normal {
263 263 file: file.try_clone().when_reading_file(path)?,
264 264 check_ambig: check_ambig.clone(),
265 265 path: path.to_owned(),
266 266 },
267 267 })
268 268 }
269 269 pub fn set_len(&self, len: u64) -> Result<(), std::io::Error> {
270 270 match self {
271 271 VfsFile::Atomic(atomic_file) => atomic_file.fp.set_len(len),
272 272 VfsFile::Normal { file, .. } => file.set_len(len),
273 273 }
274 274 }
275 275
276 276 pub fn metadata(&self) -> Result<std::fs::Metadata, std::io::Error> {
277 277 match self {
278 278 VfsFile::Atomic(atomic_file) => atomic_file.fp.metadata(),
279 279 VfsFile::Normal { file, .. } => file.metadata(),
280 280 }
281 281 }
282 282 }
283 283
284 284 impl AsRawFd for VfsFile {
285 285 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
286 286 match self {
287 287 VfsFile::Atomic(atomic_file) => atomic_file.fp.as_raw_fd(),
288 288 VfsFile::Normal { file, .. } => file.as_raw_fd(),
289 289 }
290 290 }
291 291 }
292 292
293 293 impl Seek for VfsFile {
294 294 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
295 295 match self {
296 296 VfsFile::Atomic(atomic_file) => atomic_file.seek(pos),
297 297 VfsFile::Normal { file, .. } => file.seek(pos),
298 298 }
299 299 }
300 300 }
301 301
302 302 impl Read for VfsFile {
303 303 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
304 304 match self {
305 305 VfsFile::Atomic(atomic_file) => atomic_file.fp.read(buf),
306 306 VfsFile::Normal { file, .. } => file.read(buf),
307 307 }
308 308 }
309 309 }
310 310
311 311 impl Write for VfsFile {
312 312 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
313 313 match self {
314 314 VfsFile::Atomic(atomic_file) => atomic_file.fp.write(buf),
315 315 VfsFile::Normal { file, .. } => file.write(buf),
316 316 }
317 317 }
318 318
319 319 fn flush(&mut self) -> std::io::Result<()> {
320 320 match self {
321 321 VfsFile::Atomic(atomic_file) => atomic_file.fp.flush(),
322 322 VfsFile::Normal { file, .. } => file.flush(),
323 323 }
324 324 }
325 325 }
326 326
327 327 impl Drop for VfsFile {
328 328 fn drop(&mut self) {
329 329 if let VfsFile::Normal {
330 330 path,
331 331 check_ambig: Some(old),
332 332 ..
333 333 } = self
334 334 {
335 335 avoid_timestamp_ambiguity(path, old)
336 336 }
337 337 }
338 338 }
339 339
340 340 /// Records the number of times we've fixed a timestamp ambiguity, only
341 341 /// applicable for tests.
342 342 #[cfg(test)]
343 343 static TIMESTAMP_FIXES_CALLS: AtomicUsize = AtomicUsize::new(0);
344 344
345 345 fn avoid_timestamp_ambiguity(path: &Path, old: &Metadata) {
346 346 if let Ok(new) = path.metadata() {
347 347 let is_ambiguous = is_filetime_ambiguous(&new, old);
348 348 if is_ambiguous {
349 349 let advanced =
350 350 filetime::FileTime::from_unix_time(old.mtime() + 1, 0);
351 351 if filetime::set_file_times(path, advanced, advanced).is_ok() {
352 352 #[cfg(test)]
353 353 {
354 354 TIMESTAMP_FIXES_CALLS.fetch_add(1, Ordering::Relaxed);
355 355 }
356 356 }
357 357 }
358 358 }
359 359 }
360 360
361 361 /// Examine whether new stat is ambiguous against old one
362 362 ///
363 363 /// "S[N]" below means stat of a file at N-th change:
364 364 ///
365 365 /// - S[n-1].ctime < S[n].ctime: can detect change of a file
366 366 /// - S[n-1].ctime == S[n].ctime
367 367 /// - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
368 368 /// - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
369 369 /// - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
370 370 /// - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
371 371 ///
372 372 /// Case (*2) above means that a file was changed twice or more at
373 373 /// same time in sec (= S[n-1].ctime), and comparison of timestamp
374 374 /// is ambiguous.
375 375 ///
376 376 /// Base idea to avoid such ambiguity is "advance mtime 1 sec, if
377 377 /// timestamp is ambiguous".
378 378 ///
379 379 /// But advancing mtime only in case (*2) doesn't work as
380 380 /// expected, because naturally advanced S[n].mtime in case (*1)
381 381 /// might be equal to manually advanced S[n-1 or earlier].mtime.
382 382 ///
383 383 /// Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
384 384 /// treated as ambiguous regardless of mtime, to avoid overlooking
385 385 /// by confliction between such mtime.
386 386 ///
387 387 /// Advancing mtime "if isambig(new, old)" ensures "S[n-1].mtime !=
388 388 /// S[n].mtime", even if size of a file isn't changed.
389 389 fn is_filetime_ambiguous(new: &Metadata, old: &Metadata) -> bool {
390 390 new.ctime() == old.ctime()
391 391 }
392 392
393 393 /// Writable file object that atomically updates a file
394 394 ///
395 395 /// All writes will go to a temporary copy of the original file. Call
396 396 /// [`Self::close`] when you are done writing, and [`Self`] will rename
397 397 /// the temporary copy to the original name, making the changes
398 398 /// visible. If the object is destroyed without being closed, all your
399 399 /// writes are discarded.
400 400 #[derive(Debug)]
401 401 pub struct AtomicFile {
402 402 /// The temporary file to write to
403 403 fp: std::fs::File,
404 404 /// Path of the temp file
405 405 temp_path: PathBuf,
406 406 /// Used when stat'ing the file, is useful only if the target file is
407 407 /// guarded by any lock (e.g. repo.lock or repo.wlock).
408 408 check_ambig: bool,
409 409 /// Path of the target file
410 410 target_name: PathBuf,
411 411 /// Whether the file is open or not
412 412 is_open: bool,
413 413 }
414 414
415 415 impl AtomicFile {
416 416 pub fn new(
417 417 target_path: impl AsRef<Path>,
418 418 empty: bool,
419 419 check_ambig: bool,
420 420 ) -> Result<Self, HgError> {
421 421 let target_path = target_path.as_ref().to_owned();
422 422
423 423 let random_id =
424 424 Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
425 425 let filename =
426 426 target_path.file_name().expect("target has no filename");
427 427 let filename = get_bytes_from_path(filename);
428 428 let temp_filename =
429 429 format_bytes!(b".{}-{}~", filename, random_id.as_bytes());
430 430 let temp_path =
431 431 target_path.with_file_name(get_path_from_bytes(&temp_filename));
432 432
433 433 if !empty {
434 434 std::fs::copy(&target_path, &temp_path)
435 435 .with_context(|| IoErrorContext::CopyingFile {
436 436 from: target_path.to_owned(),
437 437 to: temp_path.to_owned(),
438 438 })
439 439 // If it doesn't exist, create it on open
440 440 .io_not_found_as_none()?;
441 441 }
442 442 let fp = std::fs::OpenOptions::new()
443 443 .write(true)
444 444 .create(true)
445 445 .truncate(empty)
446 446 .open(&temp_path)
447 447 .when_writing_file(&temp_path)?;
448 448
449 449 Ok(Self {
450 450 fp,
451 451 temp_path,
452 452 check_ambig,
453 453 target_name: target_path,
454 454 is_open: true,
455 455 })
456 456 }
457 457
458 458 pub fn from_file(
459 459 fp: std::fs::File,
460 460 check_ambig: bool,
461 461 temp_name: PathBuf,
462 462 target_name: PathBuf,
463 463 ) -> Self {
464 464 Self {
465 465 fp,
466 466 check_ambig,
467 467 temp_path: temp_name,
468 468 target_name,
469 469 is_open: true,
470 470 }
471 471 }
472 472
473 473 /// Write `buf` to the temporary file
474 474 pub fn write_all(&mut self, buf: &[u8]) -> Result<(), std::io::Error> {
475 475 self.fp.write_all(buf)
476 476 }
477 477
478 478 fn target(&self) -> PathBuf {
479 479 self.temp_path
480 480 .parent()
481 481 .expect("should not be at the filesystem root")
482 482 .join(&self.target_name)
483 483 }
484 484
485 485 /// Close the temporary file and rename to the target
486 486 pub fn close(mut self) -> Result<(), std::io::Error> {
487 487 self.fp.flush()?;
488 488 let target = self.target();
489 489 if self.check_ambig {
490 490 if let Ok(stat) = target.metadata() {
491 491 std::fs::rename(&self.temp_path, &target)?;
492 492 avoid_timestamp_ambiguity(&target, &stat);
493 493 } else {
494 494 std::fs::rename(&self.temp_path, target)?;
495 495 }
496 496 } else {
497 497 std::fs::rename(&self.temp_path, target)?;
498 498 }
499 499 self.is_open = false;
500 500 Ok(())
501 501 }
502 502 }
503 503
504 504 impl Seek for AtomicFile {
505 505 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
506 506 self.fp.seek(pos)
507 507 }
508 508 }
509 509
510 510 impl Drop for AtomicFile {
511 511 fn drop(&mut self) {
512 512 if self.is_open {
513 513 std::fs::remove_file(&self.temp_path).ok();
514 514 }
515 515 }
516 516 }
517 517
518 518 /// Abstracts over the VFS to allow for different implementations of the
519 519 /// filesystem layer (like passing one from Python).
520 520 pub trait Vfs: Sync + Send + DynClone {
521 521 // TODO make `open` readonly and make `open_read` an `open_write`
522 /// Open a [`VfsFile::Normal`] for writing and reading the file at
523 /// `filename`, relative to this VFS's root.
522 524 fn open(&self, filename: &Path) -> Result<VfsFile, HgError>;
525 /// Open a [`VfsFile::Normal`] for reading the file at `filename`,
526 /// relative to this VFS's root.
523 527 fn open_read(&self, filename: &Path) -> Result<VfsFile, HgError>;
528 /// Open a [`VfsFile::Normal`] for reading and writing the file at
529 /// `filename`, relative to this VFS's root. This file will be checked
530 /// for an ambiguous mtime on [`drop`]. See [`is_filetime_ambiguous`].
524 531 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError>;
532 /// Create a [`VfsFile::Normal`] for reading and writing the file at
533 /// `filename`, relative to this VFS's root. If the file already exists,
534 /// it will be truncated to 0 bytes.
525 535 fn create(
526 536 &self,
527 537 filename: &Path,
528 538 check_ambig: bool,
529 539 ) -> Result<VfsFile, HgError>;
530 /// Must truncate the new file if exist
540 /// Create a [`VfsFile::Atomic`] for reading and writing the file at
541 /// `filename`, relative to this VFS's root. If the file already exists,
542 /// it will be truncated to 0 bytes.
531 543 fn create_atomic(
532 544 &self,
533 545 filename: &Path,
534 546 check_ambig: bool,
535 547 ) -> Result<VfsFile, HgError>;
548 /// Return the total file size in bytes of the open `file`. Errors are
549 /// usual IO errors (invalid file handle, permissions, etc.)
536 550 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError>;
551 /// Return `true` if `filename` exists relative to this VFS's root. Errors
552 /// will coerce to `false`, to this also returns `false` if there are
553 /// IO problems. This is fine because any operation that actually tries
554 /// to do anything with this path will get the same error.
537 555 fn exists(&self, filename: &Path) -> bool;
556 /// Remove the file at `filename` relative to this VFS's root. Errors
557 /// are the usual IO errors (lacking permission, file does not exist, etc.)
538 558 fn unlink(&self, filename: &Path) -> Result<(), HgError>;
559 /// Rename the file `from` to `to`, both relative to this VFS's root.
560 /// Errors are the usual IO errors (lacking permission, file does not
561 /// exist, etc.). If `check_ambig` is `true`, the VFS will check for an
562 /// ambiguous mtime on rename. See [`is_filetime_ambiguous`].
539 563 fn rename(
540 564 &self,
541 565 from: &Path,
542 566 to: &Path,
543 567 check_ambig: bool,
544 568 ) -> Result<(), HgError>;
569 /// Rename the file `from` to `to`, both relative to this VFS's root.
570 /// Errors are the usual IO errors (lacking permission, file does not
571 /// exist, etc.). If `check_ambig` is passed, the VFS will check for an
572 /// ambiguous mtime on rename. See [`is_filetime_ambiguous`].
545 573 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>;
574 /// Returns the absolute root path of this VFS, relative to which all
575 /// operations are done.
546 576 fn base(&self) -> &Path;
547 577 }
548 578
549 579 /// These methods will need to be implemented once `rhg` (and other) non-Python
550 580 /// users of `hg-core` start doing more on their own, like writing to files.
551 581 impl Vfs for VfsImpl {
552 582 fn open(&self, filename: &Path) -> Result<VfsFile, HgError> {
553 583 if self.readonly {
554 584 return Err(HgError::abort(
555 585 "write access in a readonly vfs",
556 586 exit_codes::ABORT,
557 587 None,
558 588 ));
559 589 }
560 590 // TODO auditpath
561 591 let path = self.base.join(filename);
562 592 copy_in_place_if_hardlink(&path)?;
563 593
564 594 Ok(VfsFile::normal(
565 595 OpenOptions::new()
566 596 .create(false)
567 597 .create_new(false)
568 598 .write(true)
569 599 .read(true)
570 600 .open(&path)
571 601 .when_writing_file(&path)?,
572 602 path.to_owned(),
573 603 ))
574 604 }
575 605
576 606 fn open_read(&self, filename: &Path) -> Result<VfsFile, HgError> {
577 607 // TODO auditpath
578 608 let path = self.base.join(filename);
579 609 Ok(VfsFile::normal(
580 610 std::fs::File::open(&path).when_reading_file(&path)?,
581 611 filename.to_owned(),
582 612 ))
583 613 }
584 614
585 615 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> {
586 616 if self.readonly {
587 617 return Err(HgError::abort(
588 618 "write access in a readonly vfs",
589 619 exit_codes::ABORT,
590 620 None,
591 621 ));
592 622 }
593 623
594 624 let path = self.base.join(filename);
595 625 copy_in_place_if_hardlink(&path)?;
596 626
597 627 // TODO auditpath
598 628 VfsFile::normal_check_ambig(
599 629 OpenOptions::new()
600 630 .write(true)
601 631 .read(true) // Can be used for reading to save on `open` calls
602 632 .create(false)
603 633 .open(&path)
604 634 .when_reading_file(&path)?,
605 635 path.to_owned(),
606 636 )
607 637 }
608 638
609 639 fn create(
610 640 &self,
611 641 filename: &Path,
612 642 check_ambig: bool,
613 643 ) -> Result<VfsFile, HgError> {
614 644 if self.readonly {
615 645 return Err(HgError::abort(
616 646 "write access in a readonly vfs",
617 647 exit_codes::ABORT,
618 648 None,
619 649 ));
620 650 }
621 651 // TODO auditpath
622 652 let path = self.base.join(filename);
623 653 let parent = path.parent().expect("file at root");
624 654 std::fs::create_dir_all(parent).when_writing_file(parent)?;
625 655
626 656 let file = OpenOptions::new()
627 657 .create(true)
628 658 .truncate(true)
629 659 .write(true)
630 660 .read(true)
631 661 .open(&path)
632 662 .when_writing_file(&path)?;
633 663
634 664 if let Some(mode) = self.mode {
635 665 // Creating the file with the right permission (with `.mode()`)
636 666 // may not work since umask takes effect for file creation.
637 667 // So we need to fix the permission after creating the file.
638 668 fix_directory_permissions(&self.base, &path, mode)?;
639 669 let perm = std::fs::Permissions::from_mode(mode & 0o666);
640 670 std::fs::set_permissions(&path, perm).when_writing_file(&path)?;
641 671 }
642 672
643 673 Ok(VfsFile::Normal {
644 674 file,
645 675 check_ambig: if check_ambig {
646 676 Some(path.metadata().when_reading_file(&path)?)
647 677 } else {
648 678 None
649 679 },
650 680 path: path.to_owned(),
651 681 })
652 682 }
653 683
654 684 fn create_atomic(
655 685 &self,
656 686 _filename: &Path,
657 687 _check_ambig: bool,
658 688 ) -> Result<VfsFile, HgError> {
659 689 todo!()
660 690 }
661 691
662 692 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> {
663 693 Ok(file
664 694 .metadata()
665 695 .map_err(|e| {
666 696 HgError::abort(
667 697 format!("Could not get file metadata: {}", e),
668 698 exit_codes::ABORT,
669 699 None,
670 700 )
671 701 })?
672 702 .size())
673 703 }
674 704
675 705 fn exists(&self, filename: &Path) -> bool {
676 706 self.base.join(filename).exists()
677 707 }
678 708
679 709 fn unlink(&self, filename: &Path) -> Result<(), HgError> {
680 710 if self.readonly {
681 711 return Err(HgError::abort(
682 712 "write access in a readonly vfs",
683 713 exit_codes::ABORT,
684 714 None,
685 715 ));
686 716 }
687 717 let path = self.base.join(filename);
688 718 std::fs::remove_file(&path)
689 719 .with_context(|| IoErrorContext::RemovingFile(path))
690 720 }
691 721
692 722 fn rename(
693 723 &self,
694 724 from: &Path,
695 725 to: &Path,
696 726 check_ambig: bool,
697 727 ) -> Result<(), HgError> {
698 728 if self.readonly {
699 729 return Err(HgError::abort(
700 730 "write access in a readonly vfs",
701 731 exit_codes::ABORT,
702 732 None,
703 733 ));
704 734 }
705 735 let old_stat = if check_ambig {
706 736 Some(
707 737 from.metadata()
708 738 .when_reading_file(from)
709 739 .io_not_found_as_none()?,
710 740 )
711 741 } else {
712 742 None
713 743 };
714 744 let from = self.base.join(from);
715 745 let to = self.base.join(to);
716 746 std::fs::rename(&from, &to).with_context(|| {
717 747 IoErrorContext::RenamingFile {
718 748 from,
719 749 to: to.to_owned(),
720 750 }
721 751 })?;
722 752 if let Some(Some(old)) = old_stat {
723 753 avoid_timestamp_ambiguity(&to, &old);
724 754 }
725 755 Ok(())
726 756 }
727 757
728 758 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> {
729 759 let from = self.base.join(from);
730 760 let to = self.base.join(to);
731 761 std::fs::copy(&from, &to)
732 762 .with_context(|| IoErrorContext::CopyingFile { from, to })
733 763 .map(|_| ())
734 764 }
735 765
736 766 fn base(&self) -> &Path {
737 767 &self.base
738 768 }
739 769 }
740 770
741 771 fn fix_directory_permissions(
742 772 base: &Path,
743 773 path: &Path,
744 774 mode: u32,
745 775 ) -> Result<(), HgError> {
746 776 let mut ancestors = path.ancestors();
747 777 ancestors.next(); // yields the path itself
748 778
749 779 for ancestor in ancestors {
750 780 if ancestor == base {
751 781 break;
752 782 }
753 783 let perm = std::fs::Permissions::from_mode(mode);
754 784 std::fs::set_permissions(ancestor, perm)
755 785 .when_writing_file(ancestor)?;
756 786 }
757 787 Ok(())
758 788 }
759 789
760 790 /// A VFS that understands the `fncache` store layout (file encoding), and
761 791 /// adds new entries to the `fncache`.
762 792 /// TODO Only works when using from Python for now.
763 793 pub struct FnCacheVfs {
764 794 inner: VfsImpl,
765 795 fncache: Box<dyn FnCache>,
766 796 }
767 797
768 798 impl Clone for FnCacheVfs {
769 799 fn clone(&self) -> Self {
770 800 Self {
771 801 inner: self.inner.clone(),
772 802 fncache: dyn_clone::clone_box(&*self.fncache),
773 803 }
774 804 }
775 805 }
776 806
777 807 impl FnCacheVfs {
778 808 pub fn new(
779 809 base: PathBuf,
780 810 readonly: bool,
781 811 fncache: Box<dyn FnCache>,
782 812 ) -> Self {
783 813 let inner = VfsImpl::new(base, readonly);
784 814 Self { inner, fncache }
785 815 }
786 816
787 817 fn maybe_add_to_fncache(
788 818 &self,
789 819 filename: &Path,
790 820 encoded_path: &Path,
791 821 ) -> Result<(), HgError> {
792 822 let relevant_file = (filename.starts_with("data/")
793 823 || filename.starts_with("meta/"))
794 824 && is_revlog_file(filename);
795 825 if relevant_file {
796 826 let not_load = !self.fncache.is_loaded()
797 827 && (self.exists(filename)
798 828 && self
799 829 .inner
800 830 .join(encoded_path)
801 831 .metadata()
802 832 .when_reading_file(encoded_path)?
803 833 .size()
804 834 != 0);
805 835 if !not_load {
806 836 self.fncache.add(filename);
807 837 }
808 838 };
809 839 Ok(())
810 840 }
811 841 }
812 842
813 843 impl Vfs for FnCacheVfs {
814 844 fn open(&self, filename: &Path) -> Result<VfsFile, HgError> {
815 845 let encoded = path_encode(&get_bytes_from_path(filename));
816 846 let encoded_path = get_path_from_bytes(&encoded);
817 847 self.maybe_add_to_fncache(filename, encoded_path)?;
818 848 self.inner.open(encoded_path)
819 849 }
820 850
821 851 fn open_read(&self, filename: &Path) -> Result<VfsFile, HgError> {
822 852 let encoded = path_encode(&get_bytes_from_path(filename));
823 853 let filename = get_path_from_bytes(&encoded);
824 854 self.inner.open_read(filename)
825 855 }
826 856
827 857 fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> {
828 858 let encoded = path_encode(&get_bytes_from_path(filename));
829 859 let filename = get_path_from_bytes(&encoded);
830 860 self.inner.open_check_ambig(filename)
831 861 }
832 862
833 863 fn create(
834 864 &self,
835 865 filename: &Path,
836 866 check_ambig: bool,
837 867 ) -> Result<VfsFile, HgError> {
838 868 let encoded = path_encode(&get_bytes_from_path(filename));
839 869 let encoded_path = get_path_from_bytes(&encoded);
840 870 self.maybe_add_to_fncache(filename, encoded_path)?;
841 871 self.inner.create(encoded_path, check_ambig)
842 872 }
843 873
844 874 fn create_atomic(
845 875 &self,
846 876 filename: &Path,
847 877 check_ambig: bool,
848 878 ) -> Result<VfsFile, HgError> {
849 879 let encoded = path_encode(&get_bytes_from_path(filename));
850 880 let filename = get_path_from_bytes(&encoded);
851 881 self.inner.create_atomic(filename, check_ambig)
852 882 }
853 883
854 884 fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> {
855 885 self.inner.file_size(file)
856 886 }
857 887
858 888 fn exists(&self, filename: &Path) -> bool {
859 889 let encoded = path_encode(&get_bytes_from_path(filename));
860 890 let filename = get_path_from_bytes(&encoded);
861 891 self.inner.exists(filename)
862 892 }
863 893
864 894 fn unlink(&self, filename: &Path) -> Result<(), HgError> {
865 895 let encoded = path_encode(&get_bytes_from_path(filename));
866 896 let filename = get_path_from_bytes(&encoded);
867 897 self.inner.unlink(filename)
868 898 }
869 899
870 900 fn rename(
871 901 &self,
872 902 from: &Path,
873 903 to: &Path,
874 904 check_ambig: bool,
875 905 ) -> Result<(), HgError> {
876 906 let encoded = path_encode(&get_bytes_from_path(from));
877 907 let from = get_path_from_bytes(&encoded);
878 908 let encoded = path_encode(&get_bytes_from_path(to));
879 909 let to = get_path_from_bytes(&encoded);
880 910 self.inner.rename(from, to, check_ambig)
881 911 }
882 912
883 913 fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> {
884 914 let encoded = path_encode(&get_bytes_from_path(from));
885 915 let from = get_path_from_bytes(&encoded);
886 916 let encoded = path_encode(&get_bytes_from_path(to));
887 917 let to = get_path_from_bytes(&encoded);
888 918 self.inner.copy(from, to)
889 919 }
890 920 fn base(&self) -> &Path {
891 921 self.inner.base()
892 922 }
893 923 }
894 924
895 925 /// Detects whether `path` is a hardlink and does a tmp copy + rename erase
896 926 /// to turn it into its own file. Revlogs are usually hardlinked when doing
897 927 /// a local clone, and we don't want to modify the original repo.
898 928 fn copy_in_place_if_hardlink(path: &Path) -> Result<(), HgError> {
899 929 let metadata = path.metadata().when_writing_file(path)?;
900 930 if metadata.nlink() > 0 {
901 931 // If it's hardlinked, copy it and rename it back before changing it.
902 932 let tmpdir = path.parent().expect("file at root");
903 933 let name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
904 934 let tmpfile = tmpdir.join(name);
905 935 std::fs::create_dir_all(tmpfile.parent().expect("file at root"))
906 936 .with_context(|| IoErrorContext::CopyingFile {
907 937 from: path.to_owned(),
908 938 to: tmpfile.to_owned(),
909 939 })?;
910 940 std::fs::copy(path, &tmpfile).with_context(|| {
911 941 IoErrorContext::CopyingFile {
912 942 from: path.to_owned(),
913 943 to: tmpfile.to_owned(),
914 944 }
915 945 })?;
916 946 std::fs::rename(&tmpfile, path).with_context(|| {
917 947 IoErrorContext::RenamingFile {
918 948 from: tmpfile,
919 949 to: path.to_owned(),
920 950 }
921 951 })?;
922 952 }
923 953 Ok(())
924 954 }
925 955
926 956 pub fn is_revlog_file(path: impl AsRef<Path>) -> bool {
927 957 path.as_ref()
928 958 .extension()
929 959 .map(|ext| {
930 960 ["i", "idx", "d", "dat", "n", "nd", "sda"]
931 961 .contains(&ext.to_string_lossy().as_ref())
932 962 })
933 963 .unwrap_or(false)
934 964 }
935 965
936 966 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
937 967 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
938 968 }
939 969
940 970 pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
941 971 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
942 972 }
943 973
944 974 /// Returns whether the given `path` is on a network file system.
945 975 /// Taken from `cargo`'s codebase.
946 976 #[cfg(target_os = "linux")]
947 977 pub(crate) fn is_on_nfs_mount(path: impl AsRef<Path>) -> bool {
948 978 use std::ffi::CString;
949 979 use std::mem;
950 980 use std::os::unix::prelude::*;
951 981
952 982 let path = match CString::new(path.as_ref().as_os_str().as_bytes()) {
953 983 Ok(path) => path,
954 984 Err(_) => return false,
955 985 };
956 986
957 987 unsafe {
958 988 let mut buf: libc::statfs = mem::zeroed();
959 989 let r = libc::statfs(path.as_ptr(), &mut buf);
960 990
961 991 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
962 992 }
963 993 }
964 994
965 995 /// Similar to what Cargo does; although detecting NFS (or non-local
966 996 /// file systems) _should_ be possible on other operating systems,
967 997 /// we'll just assume that mmap() works there, for now; after all,
968 998 /// _some_ functionality is better than a compile error, i.e. none at
969 999 /// all
970 1000 #[cfg(not(target_os = "linux"))]
971 1001 pub(crate) fn is_on_nfs_mount(_path: impl AsRef<Path>) -> bool {
972 1002 false
973 1003 }
974 1004
975 1005 #[cfg(test)]
976 1006 mod tests {
977 1007 use super::*;
978 1008
979 1009 #[test]
980 1010 fn test_atomic_file() {
981 1011 let dir = tempfile::tempdir().unwrap().into_path();
982 1012 let target_path = dir.join("sometargetname");
983 1013
984 1014 for empty in [true, false] {
985 1015 let file = AtomicFile::new(&target_path, empty, false).unwrap();
986 1016 assert!(file.is_open);
987 1017 let filename =
988 1018 file.temp_path.file_name().unwrap().to_str().unwrap();
989 1019 // Make sure we have a coherent temp name
990 1020 assert_eq!(filename.len(), 29, "{}", filename);
991 1021 assert!(filename.contains("sometargetname"));
992 1022
993 1023 // Make sure the temp file is created in the same folder
994 1024 assert_eq!(target_path.parent(), file.temp_path.parent());
995 1025 }
996 1026
997 1027 assert!(!target_path.exists());
998 1028 std::fs::write(&target_path, "version 1").unwrap();
999 1029 let mut file = AtomicFile::new(&target_path, false, false).unwrap();
1000 1030 file.write_all(b"version 2!").unwrap();
1001 1031 assert_eq!(
1002 1032 std::fs::read(&target_path).unwrap(),
1003 1033 b"version 1".to_vec()
1004 1034 );
1005 1035 let temp_path = file.temp_path.to_owned();
1006 1036 // test that dropping the file should discard the temp file and not
1007 1037 // affect the target path.
1008 1038 drop(file);
1009 1039 assert_eq!(
1010 1040 std::fs::read(&target_path).unwrap(),
1011 1041 b"version 1".to_vec()
1012 1042 );
1013 1043 assert!(!temp_path.exists());
1014 1044
1015 1045 let mut file = AtomicFile::new(&target_path, false, false).unwrap();
1016 1046 file.write_all(b"version 2!").unwrap();
1017 1047 assert_eq!(
1018 1048 std::fs::read(&target_path).unwrap(),
1019 1049 b"version 1".to_vec()
1020 1050 );
1021 1051 file.close().unwrap();
1022 1052 assert_eq!(
1023 1053 std::fs::read(&target_path).unwrap(),
1024 1054 b"version 2!".to_vec(),
1025 1055 "{}",
1026 1056 std::fs::read_to_string(&target_path).unwrap()
1027 1057 );
1028 1058 assert!(target_path.exists());
1029 1059 assert!(!temp_path.exists());
1030 1060 }
1031 1061
1032 1062 #[test]
1033 1063 fn test_vfs_file_check_ambig() {
1034 1064 let dir = tempfile::tempdir().unwrap().into_path();
1035 1065 let file_path = dir.join("file");
1036 1066
1037 1067 fn vfs_file_write(file_path: &Path, check_ambig: bool) {
1038 1068 let file = std::fs::OpenOptions::new()
1039 1069 .write(true)
1040 1070 .open(file_path)
1041 1071 .unwrap();
1042 1072 let old_stat = if check_ambig {
1043 1073 Some(file.metadata().unwrap())
1044 1074 } else {
1045 1075 None
1046 1076 };
1047 1077
1048 1078 let mut vfs_file = VfsFile::Normal {
1049 1079 file,
1050 1080 path: file_path.to_owned(),
1051 1081 check_ambig: old_stat,
1052 1082 };
1053 1083 vfs_file.write_all(b"contents").unwrap();
1054 1084 }
1055 1085
1056 1086 std::fs::OpenOptions::new()
1057 1087 .write(true)
1058 1088 .create(true)
1059 1089 .truncate(false)
1060 1090 .open(&file_path)
1061 1091 .unwrap();
1062 1092
1063 1093 let number_of_writes = 3;
1064 1094
1065 1095 // Try multiple times, because reproduction of an ambiguity depends
1066 1096 // on "filesystem time"
1067 1097 for _ in 0..5 {
1068 1098 TIMESTAMP_FIXES_CALLS.store(0, Ordering::Relaxed);
1069 1099 vfs_file_write(&file_path, false);
1070 1100 let old_stat = file_path.metadata().unwrap();
1071 1101 if old_stat.ctime() != old_stat.mtime() {
1072 1102 // subsequent changing never causes ambiguity
1073 1103 continue;
1074 1104 }
1075 1105
1076 1106 // Repeat atomic write with `check_ambig == true`, to examine
1077 1107 // whether the mtime is advanced multiple times as expected
1078 1108 for _ in 0..number_of_writes {
1079 1109 vfs_file_write(&file_path, true);
1080 1110 }
1081 1111 let new_stat = file_path.metadata().unwrap();
1082 1112 if !is_filetime_ambiguous(&new_stat, &old_stat) {
1083 1113 // timestamp ambiguity was naturally avoided while repetition
1084 1114 continue;
1085 1115 }
1086 1116
1087 1117 assert_eq!(
1088 1118 TIMESTAMP_FIXES_CALLS.load(Ordering::Relaxed),
1089 1119 number_of_writes
1090 1120 );
1091 1121 assert_eq!(
1092 1122 old_stat.mtime() + number_of_writes as i64,
1093 1123 file_path.metadata().unwrap().mtime()
1094 1124 );
1095 1125 break;
1096 1126 }
1097 1127 // If we've arrived here without breaking, we might not have
1098 1128 // tested anything because the platform is too slow. This test will
1099 1129 // still work on fast platforms.
1100 1130 }
1101 1131 }
General Comments 0
You need to be logged in to leave comments. Login now