Skip to main content

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;