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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! What:
//! How the in-process index update gets its result onto the landing's private index: the name of that index, a temporary file
//! no other run can share, Git's file mode for an index, and the rename.
//! Why:
//! The update replaces `index.cli-git-<batch>`, the copy of the live index the landing made under its `index.lock`; the
//! lock's release then renames that copy over the live index (`service_index_hold.rs`, `IndexLock::release_with`). The update
//! takes a borrow of the held `IndexLock`, so only the holder can call it, and it works on the one name `private_index_path`
//! gives for that lock's index and batch. Two runs on one target must not share a temporary name: each creates its
//! own file exclusively and removes only that one. Git creates an index through a lock file whose mode `adjust_shared_perm`
//! widens by `core.sharedRepository` (`tempfile.c`, `path.c`), so the file gets the same mode before it is published.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await publish(privateIndexPath(lock.index(), seq), bytes, sharedRepository); // takes the held lock as a witness
//! ```
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
/// How many times a temporary name is tried before the update gives up.
const NAME_TRIES: usize = 16;
/// A count that makes the temporary names of one process differ.
static NEXT_NAME: AtomicU64 = AtomicU64::new(0);
/// The mode `calc_shared_perm` makes of `mode` for the `core.sharedRepository` value `shared` (`path.c`).
pub fn shared_mode(shared: i32, mode: u32) -> u32 {
let mut tweak: u32 = shared.unsigned_abs();
if mode & 0o200 == 0 {
tweak &= !0o222;
}
if mode & 0o100 != 0 {
tweak |= (tweak & 0o444) >> 2;
}
return if shared < 0 { (mode & !0o777) | tweak } else { mode | tweak };
}
/// A file name beside `target` that no other run uses: the process, a count and the clock.
fn temporary_beside(target: &Path) -> Option<PathBuf> {
let count: u64 = NEXT_NAME.fetch_add(1, Ordering::Relaxed);
let clock: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_nanos();
return Some(target.with_file_name(format!("{}.reset-{}-{count}-{clock:x}", target.file_name()?.to_string_lossy(), std::process::id())));
}
/// The steps of a publication that touch the file system, so a control can make any one of them fail or run short.
pub struct Steps<'a> {
/// One write of some of the bytes; returns how many were taken.
pub write: &'a dyn Fn(&mut std::fs::File, &[u8]) -> std::io::Result<usize>,
/// The change of the open file's permission bits.
pub chmod: &'a dyn Fn(&std::fs::File, u32) -> std::io::Result<()>,
/// The flush of the file to the disk.
pub sync: &'a dyn Fn(&std::fs::File) -> std::io::Result<()>,
/// The rename of the finished file over the target.
pub rename: &'a dyn Fn(&Path, &Path) -> std::io::Result<()>,
/// The mode bits the new file has, as the file system gave them.
pub mode: &'a dyn Fn(&std::fs::File) -> std::io::Result<u32>,
}
/// The permission bits the file system gave the new file, which already include the mask and any inherited default ACL.
fn mode_of_file(file: &std::fs::File) -> std::io::Result<u32> {
return Ok(file.metadata()?.permissions().mode());
}
/// One write to the file.
fn write_some(file: &mut std::fs::File, bytes: &[u8]) -> std::io::Result<usize> {
return file.write(bytes);
}
/// The file's permission bits set with `fchmod`.
fn set_mode(file: &std::fs::File, mode: u32) -> std::io::Result<()> {
return file.set_permissions(std::fs::Permissions::from_mode(mode));
}
/// The file flushed to the disk, data and metadata (`CSUM_FSYNC`).
fn sync_all(file: &std::fs::File) -> std::io::Result<()> {
return super::service_durable::flush_index(file, "index-publish");
}
/// The rename of the finished file over the target.
fn rename_file(from: &Path, to: &Path) -> std::io::Result<()> {
return std::fs::rename(from, to);
}
/// The operating system's own calls.
pub const SYSTEM: Steps<'static> = Steps { write: &write_some, chmod: &set_mode, sync: &sync_all, rename: &rename_file, mode: &mode_of_file };
/// Create the temporary file as Git creates its index lock file and give it Git's mode before a byte is written to it: Git
/// creates the file with the mode 0666 and lets the file system narrow it by the mask and any default ACL of the directory, and
/// `adjust_shared_perm` (`tempfile.c:152`) then widens or replaces the bits it finds, so the calculation starts from the mode the
/// new file really has. Nothing is written before that, so a `core.sharedRepository` value narrower than the mask or the ACL gives
/// never lets the bytes be read more widely.
fn create_beside(steps: &Steps<'_>, target: &Path, shared: i32) -> std::io::Result<(std::fs::File, PathBuf)> {
use std::os::unix::fs::OpenOptionsExt as _;
for _ in 0..NAME_TRIES {
let Some(temporary) = temporary_beside(target) else {
break;
};
let file: std::fs::File = match std::fs::OpenOptions::new().write(true).create_new(true).mode(0o666).open(&temporary) {
Ok(created) => created,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
};
if shared != 0 {
let changed: std::io::Result<()> = (steps.mode)(&file).and_then(|gits_file| { return (steps.chmod)(&file, shared_mode(shared, gits_file) & 0o7777); });
if let Err(error) = changed {
let _removed = std::fs::remove_file(&temporary);
return Err(error);
}
}
return Ok((file, temporary));
}
return Err(std::io::Error::other("no unused temporary name beside the index"));
}
/// Write all of `bytes`, however the writes are cut.
fn write_all_with(steps: &Steps<'_>, file: &mut std::fs::File, bytes: &[u8]) -> std::io::Result<()> {
let mut rest: &[u8] = bytes;
while !rest.is_empty() {
let taken: usize = match (steps.write)(file, rest) {
Ok(count) => count,
// A signal that arrives during a write is not a failure; `write_all` tries again.
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
};
if taken == 0 {
return Err(std::io::Error::from(std::io::ErrorKind::WriteZero));
}
rest = &rest[taken..];
}
return Ok(());
}
/// What:
/// Write `bytes` to a new file of the mode Git gives an index, flush it, and rename it over `target`, through `steps`.
/// Why:
/// The file is created exclusively, so it is this run's and only this run removes it; its mode is set before any byte is
/// written; a failure at any step removes it and leaves `target` as it was.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await publishWith(steps, target, bytes, shared); // throws; the target is the old index or the whole new one
/// ```
///
/// # Errors
///
/// The input and output error of the step that failed.
pub fn publish_with(steps: &Steps<'_>, target: &Path, bytes: &[u8], shared: i32) -> std::io::Result<()> {
let (mut file, temporary) = create_beside(steps, target, shared)?;
let written: std::io::Result<()> = write_all_with(steps, &mut file, bytes).and_then(|()| { return (steps.sync)(&file); }).and_then(|()| { return (steps.rename)(&temporary, target); });
if written.is_err() {
let _removed = std::fs::remove_file(&temporary);
}
return written;
}
/// `publish_with` the operating system's calls.
///
/// # Errors
///
/// As `publish_with`.
pub fn publish(target: &Path, bytes: &[u8], shared: i32) -> std::io::Result<()> {
return publish_with(&SYSTEM, target, bytes, shared);
}
/// Controls for the publication step stay out of the release executable.
#[cfg(test)]
#[path = "index_reset_publish_tests.rs"]
mod tests;