1use std::{
4 ffi::OsString,
5 fs::{File, OpenOptions},
6 io::{self, Read, Write},
7 path::{Path, PathBuf},
8 sync::atomic::{AtomicU64, Ordering},
9};
10
11#[cfg(windows)]
12#[allow(unsafe_code)]
13mod windows;
14#[cfg(windows)]
15use windows::{open_record, replace};
16
17static NEXT: AtomicU64 = AtomicU64::new(0);
18
19struct Temporary(PathBuf);
20impl Drop for Temporary {
21 fn drop(&mut self) {
22 let _ = std::fs::remove_file(&self.0);
23 }
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct RecordOptions {
30 durable: bool,
31 #[cfg(unix)]
32 mode: u32,
33 read_limit: u64,
34}
35
36impl Default for RecordOptions {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl RecordOptions {
43 pub const fn new() -> Self {
45 Self {
46 durable: false,
47 #[cfg(unix)]
48 mode: 0o600,
49 read_limit: 64 * 1024,
50 }
51 }
52
53 pub const fn durable(mut self, durable: bool) -> Self {
62 self.durable = durable;
63 self
64 }
65
66 #[cfg(unix)]
69 pub const fn mode(mut self, mode: u32) -> Self {
70 self.mode = mode;
71 self
72 }
73
74 pub const fn read_limit(mut self, bytes: u64) -> Self {
78 self.read_limit = bytes;
79 self
80 }
81}
82
83pub fn publish_record(path: &Path, contents: &[u8]) -> io::Result<()> {
104 publish_record_with(path, contents, RecordOptions::new())
105}
106
107pub fn publish_record_with(path: &Path, contents: &[u8], options: RecordOptions) -> io::Result<()> {
109 let parent = path
110 .parent()
111 .filter(|p| !p.as_os_str().is_empty())
112 .unwrap_or_else(|| Path::new("."));
113 let filename = path.file_name().ok_or_else(|| {
114 io::Error::new(
115 io::ErrorKind::InvalidInput,
116 "discovery path needs a filename",
117 )
118 })?;
119 let (temporary, mut file): (Temporary, File) = loop {
120 let mut name = OsString::from(".");
121 name.push(filename);
122 name.push(format!(
123 ".{}.{}.tmp",
124 std::process::id(),
125 NEXT.fetch_add(1, Ordering::Relaxed)
126 ));
127 let temporary = parent.join(name);
128 let mut open = OpenOptions::new();
129 open.create_new(true).write(true);
130 #[cfg(unix)]
131 {
132 use std::os::unix::fs::OpenOptionsExt;
133 open.mode(0o600);
134 }
135 #[cfg(windows)]
136 {
137 use std::os::windows::fs::OpenOptionsExt;
138 open.access_mode(windows::ACCESS);
139 }
140 match open.open(&temporary) {
141 Ok(file) => break (Temporary(temporary), file),
142 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
143 Err(e) => return Err(e),
144 }
145 };
146 #[cfg(unix)]
147 {
148 use std::os::unix::fs::PermissionsExt;
149 file.set_permissions(std::fs::Permissions::from_mode(options.mode))?;
150 }
151 file.write_all(contents)?;
152 file.sync_all()?;
153 replace(file, &temporary.0, path, options.durable)
154}
155
156#[cfg(not(windows))]
159fn replace(file: File, from: &Path, to: &Path, durable: bool) -> io::Result<()> {
160 drop(file);
161 std::fs::rename(from, to)?;
162 if durable {
163 let directory = from.parent().expect("a temporary has a directory");
165 File::open(directory)?.sync_all()?;
166 }
167 Ok(())
168}
169
170#[cfg(not(windows))]
173fn open_record(open: &OpenOptions, path: &Path) -> io::Result<File> {
174 open.open(path)
175}
176
177fn read_regular(path: &Path, limit: u64) -> io::Result<Option<Vec<u8>>> {
185 let not_regular = || io::Error::new(io::ErrorKind::InvalidData, "record is not a regular file");
186 let mut open = OpenOptions::new();
187 open.read(true);
188 #[cfg(unix)]
189 {
190 use std::os::unix::fs::OpenOptionsExt;
191 open.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
192 }
193 #[cfg(windows)]
194 {
195 use std::os::windows::fs::OpenOptionsExt;
196 open.custom_flags(0x0220_0000);
200 }
201 let file = match open_record(&open, path) {
202 Ok(file) => file,
203 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
204 #[cfg(unix)]
206 Err(error) if error.raw_os_error() == Some(libc::ELOOP) => return Err(not_regular()),
207 Err(error) => return Err(error),
208 };
209 if !file.metadata()?.is_file() {
210 return Err(not_regular());
211 }
212 let mut contents = Vec::new();
213 file.take(limit.saturating_add(1))
214 .read_to_end(&mut contents)?;
215 if contents.len() as u64 > limit {
216 return Err(io::Error::new(
217 io::ErrorKind::InvalidData,
218 "record is longer than its read limit",
219 ));
220 }
221 Ok(Some(contents))
222}
223
224#[cfg(feature = "local")]
231pub fn ensure_private_dir(dir: &Path) -> io::Result<()> {
232 std::fs::create_dir_all(dir)?;
233 let metadata = std::fs::symlink_metadata(dir)?;
234 if !metadata.is_dir() {
235 return Err(io::ErrorKind::PermissionDenied.into());
236 }
237 #[cfg(unix)]
238 {
239 use std::os::unix::fs::{MetadataExt, PermissionsExt};
240 if metadata.uid() != crate::local::unix::own_uid() {
241 return Err(io::ErrorKind::PermissionDenied.into());
242 }
243 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
244 }
245 Ok(())
246}
247
248#[derive(Clone, Debug)]
251pub struct RecordSlot(PathBuf, RecordOptions);
252impl RecordSlot {
253 pub fn new(path: impl Into<PathBuf>) -> Self {
255 Self(path.into(), RecordOptions::new())
256 }
257
258 pub fn options(mut self, options: RecordOptions) -> Self {
260 self.1 = options;
261 self
262 }
263
264 fn lock(&self) -> io::Result<crate::ProcessLock> {
265 let mut lock = self.0.as_os_str().to_owned();
266 lock.push(".record.lock");
267 crate::ProcessLock::acquire_until(
268 &lock,
269 std::time::Instant::now() + std::time::Duration::from_secs(2),
270 )?
271 .ok_or_else(|| io::ErrorKind::TimedOut.into())
272 }
273
274 pub fn replace(&self, contents: &[u8]) -> io::Result<()> {
276 let _lock = self.lock()?;
277 publish_record_with(&self.0, contents, self.1)
278 }
279
280 pub fn repair(&self, contents: &[u8]) -> io::Result<bool> {
283 let _lock = self.lock()?;
284 if self.matches(contents)? {
285 return Ok(false);
286 }
287 publish_record_with(&self.0, contents, self.1)?;
288 Ok(true)
289 }
290
291 pub fn replace_if(
296 &self,
297 contents: &[u8],
298 replace: impl FnOnce(Option<&[u8]>) -> bool,
299 ) -> io::Result<bool> {
300 let _lock = self.lock()?;
301 let current = read_regular(&self.0, self.1.read_limit)?;
302 if !replace(current.as_deref()) {
303 return Ok(false);
304 }
305 publish_record_with(&self.0, contents, self.1)?;
306 Ok(true)
307 }
308
309 pub fn remove_if_matches(&self, contents: &[u8]) -> io::Result<bool> {
312 let _lock = self.lock()?;
313 if !self.matches(contents)? {
314 return Ok(false);
315 }
316 std::fs::remove_file(&self.0)?;
317 Ok(true)
318 }
319
320 pub fn remove_if(&self, remove: impl FnOnce(&[u8]) -> bool) -> io::Result<bool> {
323 let _lock = self.lock()?;
324 let Some(current) = read_regular(&self.0, self.1.read_limit)? else {
325 return Ok(false);
326 };
327 if !remove(¤t) {
328 return Ok(false);
329 }
330 std::fs::remove_file(&self.0)?;
331 Ok(true)
332 }
333
334 pub fn read(&self) -> io::Result<Option<Vec<u8>>> {
345 read_regular(&self.0, self.1.read_limit)
346 }
347
348 fn matches(&self, contents: &[u8]) -> io::Result<bool> {
349 let file = match open_record(OpenOptions::new().read(true), &self.0) {
350 Ok(file) => file,
351 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
352 Err(error) => return Err(error),
353 };
354 let mut current = Vec::new();
355 file.take(contents.len().saturating_add(1) as u64)
356 .read_to_end(&mut current)?;
357 Ok(current == contents)
358 }
359}