1use std::fs::{File, FileTimes};
2use std::io;
3use std::path::Path;
4use std::time::{Duration, SystemTime};
5
6use crate::error::io_error;
7use crate::{BuildId, Cache, Result, layout};
8
9const ACCESS_INTERVAL: Duration = Duration::from_secs(60 * 60);
10
11#[cfg_attr(docsrs, doc(cfg(feature = "access")))]
12impl Cache {
13 pub fn record_access(&self, build_id: &BuildId) -> Result<AccessUpdate> {
24 let directory = self.prepare_access()?;
25 let key = layout::access_key(build_id);
26 let key_path = key.as_c_str().ok_or_else(|| {
27 io_error(
28 "encode access marker path",
29 layout::access(self.base(), build_id),
30 io::Error::from(io::ErrorKind::InvalidInput),
31 )
32 })?;
33 let now = SystemTime::now();
34 match rustix::fs::statat(directory, key_path, rustix::fs::AtFlags::SYMLINK_NOFOLLOW) {
35 Ok(metadata) => {
36 if rustix::fs::FileType::from_raw_mode(metadata.st_mode)
37 != rustix::fs::FileType::RegularFile
38 || !self.owns_uid(metadata.st_uid)?
39 {
40 return Err(crate::Error::UntrustedEntry {
41 path: layout::access(self.base(), build_id),
42 });
43 }
44 if marker_is_recent(metadata.st_mtime, metadata.st_mtime_nsec, now) {
45 return Ok(AccessUpdate::Debounced);
46 }
47 }
48 Err(source) if source == rustix::io::Errno::NOENT => {}
49 Err(source) => {
50 return Err(io_error(
51 "inspect access marker",
52 layout::access(self.base(), build_id),
53 io::Error::from(source),
54 ));
55 }
56 }
57 let (file, created) = open_marker(self, directory, key_path, build_id)?;
58 if created {
59 return Ok(AccessUpdate::Recorded);
60 }
61 file.set_times(FileTimes::new().set_modified(now))
62 .map_err(|source| {
63 io_error(
64 "update access marker",
65 layout::access(self.base(), build_id),
66 source,
67 )
68 })?;
69 Ok(AccessUpdate::Recorded)
70 }
71
72 pub(crate) fn prepare_access(&self) -> Result<&File> {
73 if let Some(directory) = self.access_directory.get() {
74 return Ok(directory);
75 }
76 if let Some((cache_home, application)) = self.xdg_paths() {
77 crate::lookup::ensure_xdg_anchor(cache_home)?;
78 crate::lookup::ensure_xdg_anchor(application)?;
79 }
80 crate::lookup::ensure_private_directory(self.root())?;
81 let _ = self.ensure_identity()?;
82 crate::lookup::ensure_private_directory(self.base())?;
83 let path = self.base().join(layout::ACCESS).join(layout::BUILD_ID);
84 let directory = crate::lookup::open_private_directory(&path)?;
85 drop(self.access_directory.set(directory));
86 self.access_directory
87 .get()
88 .ok_or_else(|| crate::Error::InsecureDirectory {
89 path: self.base().join(layout::ACCESS).join(layout::BUILD_ID),
90 })
91 }
92}
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96#[must_use]
97#[cfg_attr(docsrs, doc(cfg(feature = "access")))]
98pub enum AccessUpdate {
99 Recorded,
101 Debounced,
103}
104
105pub(crate) fn ensure_parent(path: &Path) -> Result<(&Path, File)> {
106 let parent = path.parent().ok_or_else(|| {
107 io_error(
108 "resolve cache entry parent",
109 path,
110 io::Error::from(io::ErrorKind::InvalidInput),
111 )
112 })?;
113 let directory = crate::lookup::open_private_directory(parent)?;
114 Ok((parent, directory))
115}
116
117fn open_marker(
118 cache: &Cache,
119 directory: &File,
120 key: &std::ffi::CStr,
121 build_id: &BuildId,
122) -> Result<(File, bool)> {
123 use rustix::fs::{Mode, OFlags};
124
125 let mut repaired_parent = false;
126 loop {
127 match rustix::fs::openat(
128 directory,
129 key,
130 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
131 Mode::empty(),
132 ) {
133 Ok(file) => {
134 let file = File::from(file);
135 let path = layout::access(cache.base(), build_id);
136 cache.validate_file(&path, &file)?;
137 return Ok((file, false));
138 }
139 Err(source) if source == rustix::io::Errno::NOENT => {
140 match rustix::fs::openat(
141 directory,
142 key,
143 OFlags::RDONLY
144 | OFlags::CLOEXEC
145 | OFlags::NOFOLLOW
146 | OFlags::NONBLOCK
147 | OFlags::CREATE
148 | OFlags::EXCL,
149 Mode::RUSR | Mode::WUSR,
150 ) {
151 Ok(file) => return Ok((File::from(file), true)),
152 Err(source) if source == rustix::io::Errno::EXIST => {}
153 Err(source) if source == rustix::io::Errno::NOENT && !repaired_parent => {
154 let path = layout::access(cache.base(), build_id);
155 let _parent = ensure_parent(&path)?;
156 repaired_parent = true;
157 }
158 Err(source) => {
159 return Err(io_error(
160 "create access marker",
161 layout::access(cache.base(), build_id),
162 io::Error::from(source),
163 ));
164 }
165 }
166 }
167 Err(source) => {
168 return Err(io_error(
169 "open access marker",
170 layout::access(cache.base(), build_id),
171 io::Error::from(source),
172 ));
173 }
174 }
175 }
176}
177
178fn marker_is_recent<S, N>(seconds: S, nanoseconds: N, now: SystemTime) -> bool
179where
180 S: TryInto<u64>,
181 N: TryInto<u32>,
182{
183 let Some(modified) = system_time_from_unix(seconds, nanoseconds) else {
184 return false;
185 };
186 now.duration_since(modified)
187 .is_ok_and(|age| age < ACCESS_INTERVAL)
188}
189
190pub(crate) fn system_time_from_unix<S, N>(seconds: S, nanoseconds: N) -> Option<SystemTime>
191where
192 S: TryInto<u64>,
193 N: TryInto<u32>,
194{
195 let Ok(seconds) = seconds.try_into() else {
196 return None;
197 };
198 let Ok(nanoseconds) = nanoseconds.try_into() else {
199 return None;
200 };
201 SystemTime::UNIX_EPOCH.checked_add(Duration::new(seconds, nanoseconds))
202}