git_policy_cli/recovery_files.rs
1//! What:
2//! No-follow file operations recovery and landing share:
3//! existence probes,
4//! exact reads,
5//! byte comparison,
6//! owner-preserving hard links,
7//! proof that a real `index.lock` is the
8//! exact file a transaction created,
9//! and installing an index through such a lock.
10//! Why:
11//! Recovery removes or installs through a real `index.lock` only when the journal proves the
12//! lock is the transaction's own,
13//! by device,
14//! inode and filesystem identity,
15//! never by path;
16//! the incumbent's `commit-transaction-recovery-files.ts`,
17//! `-validation.ts` and
18//! `commit-transaction-install-link.ts` define these steps.
19//!
20//! In TS you'd write (pseudocode):
21//! ```ts
22//! await installRecoveredIndex({ lockPath, realIndexPath, postIndexPath, journal });
23//! ```
24
25/// Debug diagnostics.
26use super::diagnostic_log::debug;
27/// The filesystem identity recorded with a lock.
28use super::filesystem_identity::filesystem_identity;
29/// Private reads and directory syncs.
30use super::private_storage::{ReadRefusal, read_regular_file, sync_directory};
31/// The fail-closed recovery failure.
32use super::recovery_error::{RecoveryError, io_failure};
33/// The recorded identity of a real `index.lock`.
34use super::transaction_journal::LockIdentity;
35/// The trait that gives files `.write_all`.
36use std::io::Write;
37/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
38use std::path::{Path, PathBuf};
39
40/// Largest recovery artifact read whole:
41/// indexes of very large repositories stay well below it.
42pub const RECOVERY_FILE_LIMIT: u64 = 2 * 1024 * 1024 * 1024;
43
44/// What:
45/// Whether anything exists at `path`,
46/// following links,
47/// as Node's `access` does.
48/// Any error other than "missing" is reported.
49/// Why:
50/// The incumbent's `recoveryPathExists`.
51///
52/// In TS you'd write (pseudocode):
53/// ```ts
54/// async function recoveryPathExists(path: string): Promise<boolean>;
55/// ```
56pub fn recovery_path_exists(path: &Path) -> Result<bool, RecoveryError> {
57 match std::fs::metadata(path) {
58 Ok(_) => return Ok(true),
59 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
60 Err(error) => return Err(io_failure("probing", path, &error)),
61 }
62}
63
64/// What:
65/// Whether anything exists at `path` without following a final link.
66/// Why:
67/// The incumbent's `pathPresent` and `pathExists` (both `lstat`).
68///
69/// In TS you'd write (pseudocode):
70/// ```ts
71/// async function pathPresent(path: string): Promise<boolean>;
72/// ```
73pub fn path_present(path: &Path) -> Result<bool, RecoveryError> {
74 match std::fs::symlink_metadata(path) {
75 Ok(_) => return Ok(true),
76 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
77 Err(error) => return Err(io_failure("probing", path, &error)),
78 }
79}
80
81/// What:
82/// The exact bytes of a regular recovery file,
83/// read without following a link.
84/// Why:
85/// `readRegularRecoveryFile`:
86/// a link or a non-regular file is unsafe state.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// async function readRegularRecoveryFile(path: string): Promise<Uint8Array>;
91/// ```
92pub fn read_recovery_file(path: &Path) -> Result<Vec<u8>, RecoveryError> {
93 match read_regular_file(path, RECOVERY_FILE_LIMIT) {
94 Ok(bytes) => return Ok(bytes),
95 Err(ReadRefusal::Missing) => {
96 return Err(RecoveryError(format!(
97 "Transaction recovery file is missing: {}",
98 path.display()
99 )));
100 }
101 Err(ReadRefusal::NotRegular | ReadRefusal::TooLarge) => {
102 return Err(RecoveryError(format!(
103 "Unsafe transaction recovery file: {}",
104 path.display()
105 )));
106 }
107 Err(ReadRefusal::Io(error)) => return Err(io_failure("reading", path, &error)),
108 }
109}
110
111/// What:
112/// The device and inode of a path as decimal text,
113/// without following a link,
114/// with
115/// whether it is a regular file.
116/// Why:
117/// Identities are recorded and compared as the incumbent's `String(metadata.dev)`.
118///
119/// In TS you'd write (pseudocode):
120/// ```ts
121/// const { dev, ino } = await lstat(path, { bigint: true }); [String(dev), String(ino)]
122/// ```
123pub fn file_identity_of(path: &Path) -> std::io::Result<(String, String, bool)> {
124 let metadata: std::fs::Metadata = std::fs::symlink_metadata(path)?;
125 return Ok(metadata_identity(&metadata));
126}
127
128/// What:
129/// The device,
130/// inode and regular-file flag of metadata.
131/// Why:
132/// Shared by path and descriptor identities.
133///
134/// In TS you'd write (pseudocode):
135/// ```ts
136/// [String(metadata.dev), String(metadata.ino), metadata.isFile()]
137/// ```
138pub fn metadata_identity(metadata: &std::fs::Metadata) -> (String, String, bool) {
139 #[cfg(unix)]
140 {
141 use std::os::unix::fs::MetadataExt;
142 return (
143 metadata.dev().to_string(),
144 metadata.ino().to_string(),
145 metadata.file_type().is_file(),
146 );
147 }
148 // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
149 #[cfg(not(unix))]
150 {
151 // Windows has no stable device and inode in the standard library; the identity is
152 // recorded as unknown, so a recovery there fails closed instead of guessing.
153 return (String::new(), String::new(), metadata.file_type().is_file());
154 }
155}
156
157/// What:
158/// Whether two files hold the same bytes,
159/// streamed in fixed chunks through no-follow
160/// handles;
161/// a file that cannot be opened is a failure.
162/// Why:
163/// `snapshotFilesEqual`:
164/// recovery compares the real index with recorded snapshots,
165/// which
166/// can be large,
167/// so neither is read whole.
168///
169/// In TS you'd write (pseudocode):
170/// ```ts
171/// async function snapshotFilesEqual({ leftPath, rightPath }): Promise<boolean>;
172/// ```
173pub fn files_equal(left: &Path, right: &Path) -> Result<bool, RecoveryError> {
174 let mut first: std::fs::File = open_no_follow(left)?;
175 let mut second: std::fs::File = open_no_follow(right)?;
176 // `mut` allows reusing both buffers for every chunk.
177 let mut first_buffer: Vec<u8> = vec![0; COMPARISON_CHUNK];
178 let mut second_buffer: Vec<u8> = vec![0; COMPARISON_CHUNK];
179 loop {
180 let first_count: usize = match fill(&mut first, first_buffer.as_mut_slice()) {
181 Ok(count) => count,
182 Err(error) => return Err(io_failure("reading", left, &error)),
183 };
184 let second_count: usize = match fill(&mut second, second_buffer.as_mut_slice()) {
185 Ok(count) => count,
186 Err(error) => return Err(io_failure("reading", right, &error)),
187 };
188 if first_count != second_count
189 || first_buffer[..first_count] != second_buffer[..second_count]
190 {
191 return Ok(false);
192 }
193 if first_count < COMPARISON_CHUNK {
194 return Ok(true);
195 }
196 }
197}
198
199/// Bytes compared per chunk.
200const COMPARISON_CHUNK: usize = 64 * 1024;
201
202/// What:
203/// Read until `buffer` is full or the file ends;
204/// returns how many bytes were read.
205/// Why:
206/// A short read must not end the comparison early.
207///
208/// In TS you'd write (pseudocode):
209/// ```ts
210/// let total = 0; while (total < buffer.length) { const { bytesRead } = await handle.read(buffer, total); if (!bytesRead) break; total += bytesRead; }
211/// ```
212fn fill(file: &mut std::fs::File, buffer: &mut [u8]) -> std::io::Result<usize> {
213 use std::io::Read;
214 let mut total: usize = 0;
215 while total < buffer.len() {
216 let count: usize = file.read(&mut buffer[total..])?;
217 if count == 0 {
218 break;
219 }
220 total += count;
221 }
222 return Ok(total);
223}
224
225/// What:
226/// Open an existing file for reading without following a final link.
227/// Why:
228/// Snapshots and the real index are compared as the files themselves.
229///
230/// In TS you'd write (pseudocode):
231/// ```ts
232/// await open(path, O_RDONLY | O_NOFOLLOW)
233/// ```
234fn open_no_follow(path: &Path) -> Result<std::fs::File, RecoveryError> {
235 let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
236 options.read(true);
237 #[cfg(unix)]
238 {
239 use std::os::unix::fs::OpenOptionsExt;
240 options.custom_flags(libc::O_NOFOLLOW);
241 }
242 match options.open(path) {
243 Ok(file) => return Ok(file),
244 Err(error) => return Err(io_failure("opening", path, &error)),
245 }
246}
247
248/// What:
249/// A file's bytes,
250/// following links as Node's `readFile` does,
251/// or nothing when missing.
252/// Why:
253/// Conclusion entries and index snapshots may be absent.
254///
255/// In TS you'd write (pseudocode):
256/// ```ts
257/// async function readOptional(path: string): Promise<Uint8Array | typeof ENTRY_ABSENT>;
258/// ```
259pub fn read_optional(path: &Path) -> Result<Option<Vec<u8>>, RecoveryError> {
260 match std::fs::read(path) {
261 Ok(bytes) => return Ok(Some(bytes)),
262 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
263 Err(error) => return Err(io_failure("reading", path, &error)),
264 }
265}
266
267/// What:
268/// Hard-link `source` at `linked` and prove the new name is the expected regular file.
269/// Why:
270/// `createOwnedFileLink`:
271/// an install or a stabilized artifact keeps the exact inode,
272/// so a
273/// file swapped in under the old name is detected instead of installed.
274///
275/// In TS you'd write (pseudocode):
276/// ```ts
277/// async function createOwnedFileLink({ sourcePath, linkedPath, expectedDevice, expectedInode }): Promise<void>;
278/// ```
279pub fn create_owned_file_link(
280 source: &Path,
281 linked: &Path,
282 expected_device: &str,
283 expected_inode: &str,
284) -> Result<(), RecoveryError> {
285 remove_file_if_present(linked)?;
286 if let Err(error) = std::fs::hard_link(source, linked) {
287 return Err(io_failure("linking", linked, &error));
288 }
289 let matches: bool = match file_identity_of(linked) {
290 Ok((device, inode, regular)) => {
291 regular && device == expected_device && inode == expected_inode
292 }
293 Err(_) => false,
294 };
295 if !matches {
296 remove_file_if_present(linked)?;
297 return Err(RecoveryError(format!(
298 "Commit transaction file link identity changed: {}",
299 linked.display()
300 )));
301 }
302 return Ok(());
303}
304
305/// What:
306/// Remove one file,
307/// treating a missing one as removed.
308/// Why:
309/// `rm(path, { force: true })` for single files.
310///
311/// In TS you'd write (pseudocode):
312/// ```ts
313/// await rm(path, { force: true });
314/// ```
315pub fn remove_file_if_present(path: &Path) -> Result<(), RecoveryError> {
316 match std::fs::remove_file(path) {
317 Ok(()) => return Ok(()),
318 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
319 Err(error) => return Err(io_failure("removing", path, &error)),
320 }
321}
322
323/// What:
324/// Why `lock_path` is not the exact `index.lock` the journal recorded,
325/// or nothing when it
326/// is:
327/// a regular file,
328/// not a link,
329/// with the recorded device,
330/// inode and filesystem identity.
331/// A failure to inspect it is an error,
332/// never a mismatch.
333/// Why:
334/// The incumbent's `assertOwnedLock` throws its recovery error only for a mismatch;
335/// an
336/// `lstat` or filesystem-identity failure propagates,
337/// and `releaseOwnedLock` treats only the
338/// mismatch as another owner's lock.
339///
340/// In TS you'd write (pseudocode):
341/// ```ts
342/// try { await assertOwnedLock({ journal, lockPath }); return undefined; } catch (e) { if (e instanceof CommitTransactionRecoveryError) return e.message; throw e; }
343/// ```
344pub fn owned_lock_mismatch(
345 journal: &LockIdentity,
346 lock_path: &Path,
347) -> Result<Option<String>, RecoveryError> {
348 return owned_lock_mismatch_with(journal, lock_path, filesystem_identity);
349}
350
351/// What:
352/// The question of `owned_lock_mismatch` with the filesystem-identity mechanism passed in.
353/// Why:
354/// A lock another process removes between the file inspection and the identity lookup is a race
355/// that a control can only reproduce by replacing the mechanism.
356///
357/// In TS you'd write (pseudocode):
358/// ```ts
359/// async function ownedLockMismatch({ journal, lockPath, resolveFsId }): Promise<string | undefined>;
360/// ```
361fn owned_lock_mismatch_with(
362 journal: &LockIdentity,
363 lock_path: &Path,
364 identity_of_filesystem: fn(&Path) -> Option<String>,
365) -> Result<Option<String>, RecoveryError> {
366 let (device, inode, regular) = match file_identity_of(lock_path) {
367 Ok(found) => found,
368 Err(error) => return Err(io_failure("inspecting the index lock", lock_path, &error)),
369 };
370 if !regular {
371 return Ok(Some(format!(
372 "Index lock is not a regular owned file: {}",
373 lock_path.display()
374 )));
375 }
376 let Some(filesystem) = identity_of_filesystem(lock_path) else {
377 return Err(RecoveryError(format!(
378 "unable to resolve the filesystem identity for {}",
379 lock_path.display()
380 )));
381 };
382 if filesystem != journal.fs_id || device != journal.file.device || inode != journal.file.inode {
383 return Ok(Some(format!(
384 "Index lock identity changed: {}",
385 lock_path.display()
386 )));
387 }
388 return Ok(None);
389}
390
391/// What:
392/// Require `lock_path` to be the exact `index.lock` the journal recorded.
393/// Why:
394/// `assertOwnedLock`:
395/// cli-git never deletes or installs through a foreign lock.
396///
397/// In TS you'd write (pseudocode):
398/// ```ts
399/// async function assertOwnedLock({ journal, lockPath }): Promise<void>; // throws when foreign
400/// ```
401pub fn assert_owned_lock(journal: &LockIdentity, lock_path: &Path) -> Result<(), RecoveryError> {
402 if let Some(mismatch) = owned_lock_mismatch(journal, lock_path)? {
403 return Err(RecoveryError(mismatch));
404 }
405 return Ok(());
406}
407
408/// What:
409/// What happened when a lock the transaction no longer needs was released.
410/// Why:
411/// Callers log it;
412/// only an owned lock is removed.
413///
414/// In TS you'd write (pseudocode):
415/// ```ts
416/// type OwnedLockRelease = 'released' | 'absent' | 'foreign';
417/// ```
418#[derive(Clone, Copy, Debug, Eq, PartialEq)]
419pub enum LockRelease {
420 /// The owned lock was removed.
421 Released,
422 /// No lock was there.
423 Absent,
424 /// Another owner's lock is there now and was left in place.
425 Foreign,
426}
427
428/// What:
429/// The incumbent's name of a release outcome.
430/// Why:
431/// Debug lines read `attempt 1 lock released`,
432/// as the incumbent's do.
433///
434/// In TS you'd write (pseudocode):
435/// ```ts
436/// release // 'released' | 'absent' | 'foreign'
437/// ```
438pub fn lock_release_name(release: LockRelease) -> &'static str {
439 match release {
440 LockRelease::Released => return "released",
441 LockRelease::Absent => return "absent",
442 LockRelease::Foreign => return "foreign",
443 }
444}
445
446/// What:
447/// Remove the real-index lock only when it is still the exact file the transaction made.
448/// Why:
449/// `releaseOwnedLock`:
450/// a lock another process holds now is left alone.
451///
452/// In TS you'd write (pseudocode):
453/// ```ts
454/// async function releaseOwnedLock({ journal, lockPath }): Promise<OwnedLockRelease>;
455/// ```
456pub fn release_owned_lock(
457 journal: &LockIdentity,
458 lock_path: &Path,
459) -> Result<LockRelease, RecoveryError> {
460 return release_owned_lock_with(journal, lock_path, filesystem_identity);
461}
462
463/// What:
464/// The release of `release_owned_lock` with the filesystem-identity mechanism passed in.
465/// Why:
466/// Controls reproduce a concurrent recoverer removing the lock mid-release by replacing the mechanism.
467///
468/// In TS you'd write (pseudocode):
469/// ```ts
470/// async function releaseOwnedLock({ journal, lockPath, resolveFsId }): Promise<OwnedLockRelease>;
471/// ```
472fn release_owned_lock_with(
473 journal: &LockIdentity,
474 lock_path: &Path,
475 identity_of_filesystem: fn(&Path) -> Option<String>,
476) -> Result<LockRelease, RecoveryError> {
477 if !recovery_path_exists(lock_path)? {
478 return Ok(LockRelease::Absent);
479 }
480 let proof: Result<Option<String>, RecoveryError> =
481 owned_lock_mismatch_with(journal, lock_path, identity_of_filesystem);
482 let proven: Option<String> = match proof {
483 Ok(found) => found,
484 Err(error) => {
485 // Another recoverer of the same dead transaction may have released the lock after the existence
486 // probe; a lock that is gone now was released by that peer, whereas one that still exists but
487 // cannot be identified stays an error.
488 if recovery_path_exists(lock_path)? {
489 return Err(error);
490 }
491 debug(
492 "releaseOwnedLock",
493 format!("lock released by another recoverer: {}", lock_path.display()).as_str(),
494 );
495 return Ok(LockRelease::Absent);
496 }
497 };
498 if let Some(mismatch) = proven {
499 debug(
500 "releaseOwnedLock",
501 format!("leaving lock another owner holds: {mismatch}").as_str(),
502 );
503 return Ok(LockRelease::Foreign);
504 }
505 if let Err(error) = std::fs::remove_file(lock_path) {
506 if error.kind() == std::io::ErrorKind::NotFound {
507 debug(
508 "releaseOwnedLock",
509 format!("lock released by another recoverer: {}", lock_path.display()).as_str(),
510 );
511 return Ok(LockRelease::Absent);
512 }
513 return Err(io_failure(
514 "removing the owned index lock",
515 lock_path,
516 &error,
517 ));
518 }
519 debug(
520 "releaseOwnedLock",
521 format!("released owned lock {}", lock_path.display()).as_str(),
522 );
523 return Ok(LockRelease::Released);
524}
525
526/// What:
527/// Give an open file the access and modification times of `source`.
528/// Why:
529/// `applyIndexTimestamps`:
530/// an installed index keeps the times of the index it was
531/// computed from,
532/// so Git's racy-entry protection still re-hashes same-second edits.
533///
534/// In TS you'd write (pseudocode):
535/// ```ts
536/// await applyIndexTimestamps({ sourcePath, handle });
537/// ```
538pub fn apply_timestamps(source: &Path, file: &std::fs::File) -> std::io::Result<()> {
539 let metadata: std::fs::Metadata = std::fs::metadata(source)?;
540 let times: std::fs::FileTimes = std::fs::FileTimes::new()
541 .set_accessed(metadata.accessed()?)
542 .set_modified(metadata.modified()?);
543 return file.set_times(times);
544}
545
546/// What:
547/// Install a recorded post-index through the exact owned lock:
548/// write it into the lock,
549/// link the lock under a private name,
550/// rename that name over the real index,
551/// then remove
552/// the lock.
553/// Why:
554/// `installRecoveredIndex`:
555/// the install never goes through the mutable lock path,
556/// and the
557/// lock is proven to be the owned one before and after each step.
558///
559/// In TS you'd write (pseudocode):
560/// ```ts
561/// async function installRecoveredIndex({ lockPath, realIndexPath, postIndexPath, journal }): Promise<void>;
562/// ```
563pub fn install_recovered_index(
564 lock_path: &Path,
565 real_index: &Path,
566 post_index: &Path,
567 journal: &LockIdentity,
568) -> Result<(), RecoveryError> {
569 let bytes: Vec<u8> = read_recovery_file(post_index)?;
570 let mut lock: std::fs::File = open_lock_no_follow(lock_path)?;
571 let opened: std::fs::Metadata = match lock.metadata() {
572 Ok(metadata) => metadata,
573 Err(error) => return Err(io_failure("inspecting the index lock", lock_path, &error)),
574 };
575 let (device, inode, _) = metadata_identity(&opened);
576 if device != journal.file.device || inode != journal.file.inode {
577 return Err(RecoveryError(format!(
578 "Index lock identity changed: {}",
579 lock_path.display()
580 )));
581 }
582 let written: std::io::Result<()> = write_lock_contents(&mut lock, bytes.as_slice(), post_index);
583 if let Err(error) = written {
584 return Err(io_failure(
585 "writing the recovered index into",
586 lock_path,
587 &error,
588 ));
589 }
590 assert_owned_lock(journal, lock_path)?;
591 let install: PathBuf = parent_of(post_index).join("install.index");
592 create_owned_file_link(
593 lock_path,
594 install.as_path(),
595 journal.file.device.as_str(),
596 journal.file.inode.as_str(),
597 )?;
598 drop(lock);
599 if let Err(error) = std::fs::rename(install.as_path(), real_index) {
600 return Err(io_failure(
601 "installing the recovered index",
602 real_index,
603 &error,
604 ));
605 }
606 assert_owned_lock(journal, lock_path)?;
607 if let Err(error) = std::fs::remove_file(lock_path) {
608 return Err(io_failure(
609 "removing the owned index lock",
610 lock_path,
611 &error,
612 ));
613 }
614 let parent: &Path = parent_of(real_index);
615 if let Err(error) = sync_directory(parent) {
616 return Err(io_failure("syncing", parent, &error));
617 }
618 return Ok(());
619}
620
621/// What:
622/// The parent directory of a path,
623/// or the path itself when it has none.
624/// Why:
625/// Recorded paths are absolute files,
626/// so they always have a parent;
627/// a path without one
628/// names itself instead of panicking.
629///
630/// In TS you'd write (pseudocode):
631/// ```ts
632/// dirname(path)
633/// ```
634fn parent_of(path: &Path) -> &Path {
635 match path.parent() {
636 Some(parent) => return parent,
637 None => return path,
638 }
639}
640
641/// What:
642/// Open an existing lock for reading and writing without following a link.
643/// Why:
644/// The lock must be the file itself,
645/// never a planted link.
646///
647/// In TS you'd write (pseudocode):
648/// ```ts
649/// await open(lockPath, O_RDWR | O_NOFOLLOW)
650/// ```
651fn open_lock_no_follow(lock_path: &Path) -> Result<std::fs::File, RecoveryError> {
652 let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
653 options.read(true).write(true);
654 #[cfg(unix)]
655 {
656 use std::os::unix::fs::OpenOptionsExt;
657 options.custom_flags(libc::O_NOFOLLOW);
658 }
659 match options.open(lock_path) {
660 Ok(file) => return Ok(file),
661 Err(error) => return Err(io_failure("opening the index lock", lock_path, &error)),
662 }
663}
664
665/// What:
666/// Replace a lock's contents with `bytes`,
667/// give it the source's times,
668/// and sync it.
669/// Why:
670/// The steps between the identity checks of `install_recovered_index`.
671///
672/// In TS you'd write (pseudocode):
673/// ```ts
674/// await lock.truncate(0); await lock.writeFile(bytes); await applyIndexTimestamps(...); await lock.sync();
675/// ```
676fn write_lock_contents(
677 lock: &mut std::fs::File,
678 bytes: &[u8],
679 source: &Path,
680) -> std::io::Result<()> {
681 lock.set_len(0)?;
682 lock.write_all(bytes)?;
683 apply_timestamps(source, lock)?;
684 return lock.sync_all();
685}
686
687/// What:
688/// The path of Git's owner PID file for a lock,
689/// `<base>~pid.lock` beside `<base>.lock`.
690/// Why:
691/// With `core.lockfilePid=true`,
692/// Git writes `index~pid.lock` beside `index.lock`;
693/// the
694/// incumbent's `lockPidPath` names it this way.
695///
696/// In TS you'd write (pseudocode):
697/// ```ts
698/// lockPidPath('/repo/.git/index') // '/repo/.git/index~pid.lock'
699/// ```
700pub fn lock_pid_path(real_index: &Path) -> PathBuf {
701 // `as_os_str().to_owned()` copies the raw path so the suffix can be appended.
702 let mut name: std::ffi::OsString = real_index.as_os_str().to_owned();
703 name.push("~pid.lock");
704 return PathBuf::from(name);
705}
706
707/// Recovery file controls stay out of the release executable.
708#[cfg(test)]
709#[path = "recovery_files_tests.rs"]
710pub(crate) mod tests;