1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! What:
//! The witness of the live index file as the landing found it under the index lock, recorded in the journal, and the comparison
//! a replay makes with it.
//! Why:
//! An index update that never installed is replayed by a restart after the lock was given up, and another session may have
//! staged into the index since. What the index holds at a path cannot tell a path nobody staged from a path staged back to the
//! same entry (stage another version, then the old one again): both read the same. The file itself can tell. Every program
//! that changes an index writes a new file and renames it over the old one, so a file that is still the one the landing saw
//! (same inode, size, change time, modification time and checksum) has been written by nobody since, and a replay over it
//! discards nothing. Any other file might hold staging, so the replay is not made over it
//! (`doc/planning/cli-git-main-commit-within-native-plus-five.md`, "A replay only over the index the landing saw").
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const same = sameFile(await witnessOf(index), recordedWitness) // true: nobody wrote the index since the landing saw it
//! ```
use std::io::{Read as _, Seek as _, SeekFrom};
use std::os::unix::fs::MetadataExt as _;
use std::path::Path;
/// How many bytes at the end of the index file the witness keeps: the checksum of either object format (SHA-1 and SHA-256),
/// which the file ends with.
const TRAILER_BYTES: u64 = 32;
/// What the landing saw of the live index file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum IndexWitness {
/// There was no index file.
Absent,
/// There was this file.
File {
/// The inode number.
inode: u64,
/// The size in bytes.
size: u64,
/// The modification time, seconds and nanoseconds.
modified: (i64, i64),
/// The time of the last change of the inode, seconds and nanoseconds; a rename onto the path moves it.
changed: (i64, i64),
/// The last bytes of the file, in hexadecimal.
trailer: String,
},
}
/// The hexadecimal text of `bytes`.
fn hex(bytes: &[u8]) -> String {
return bytes.iter().map(|byte| { return format!("{byte:02x}"); }).collect();
}
/// What:
/// The witness of the index file at `index`.
/// Why:
/// The landing records it while it holds the index lock and a replay reads it again under the lock it takes, so the two are
/// compared by one function.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const witness = await witnessOf(indexPath) // 'absent' when there is no file
/// ```
///
/// # Errors
///
/// The error of the open, the metadata or the read; a file that is not there is `Absent`, not an error.
pub fn witness_of(index: &Path) -> std::io::Result<IndexWitness> {
let mut file: std::fs::File = match std::fs::File::open(index) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(IndexWitness::Absent),
Err(error) => return Err(error),
};
// The metadata of the open file, so the inode and the end of the file are of one file even if the path is replaced meanwhile.
let metadata: std::fs::Metadata = file.metadata()?;
let size: u64 = metadata.len();
let kept: u64 = size.min(TRAILER_BYTES);
file.seek(SeekFrom::Start(size - kept))?;
let mut trailer: Vec<u8> = vec![0; kept as usize];
file.read_exact(trailer.as_mut_slice())?;
return Ok(IndexWitness::File {
inode: metadata.ino(),
size,
modified: (metadata.mtime(), metadata.mtime_nsec()),
changed: (metadata.ctime(), metadata.ctime_nsec()),
trailer: hex(trailer.as_slice()),
});
}
/// The text of a witness: `absent`, or `file` and its fields separated by colons.
pub fn encode(witness: &IndexWitness) -> String {
return match witness {
IndexWitness::Absent => String::from("absent"),
IndexWitness::File { inode, size, modified, changed, trailer } => {
format!("file:{inode}:{size}:{}:{}:{}:{}:{trailer}", modified.0, modified.1, changed.0, changed.1)
}
};
}
/// The witness a text spells, or nothing when it is damaged.
pub fn decode(text: &str) -> Option<IndexWitness> {
if text == "absent" {
return Some(IndexWitness::Absent);
}
let mut fields = text.split(':');
if fields.next()? != "file" {
return None;
}
let inode: u64 = fields.next()?.parse().ok()?;
let size: u64 = fields.next()?.parse().ok()?;
let modified: (i64, i64) = (fields.next()?.parse().ok()?, fields.next()?.parse().ok()?);
let changed: (i64, i64) = (fields.next()?.parse().ok()?, fields.next()?.parse().ok()?);
let trailer: String = fields.next()?.to_string();
if fields.next().is_some() || !trailer.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) {
return None;
}
return Some(IndexWitness::File { inode, size, modified, changed, trailer });
}
#[cfg(test)]
#[path = "service_land_start_tests.rs"]
mod tests;