Skip to main content

gsym_cache/
access.rs

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    /// Records a cache hit without changing the immutable GSYM file's mtime.
14    ///
15    /// Marker updates are limited to once per hour to avoid write traffic on
16    /// repeated hits. Callers should invoke this only after a successful
17    /// [`Cache::lookup`]; recording a missing build ID creates an orphan marker
18    /// that a later scrub pass will remove.
19    ///
20    /// # Errors
21    ///
22    /// Returns an error when the marker cannot be inspected or updated.
23    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/// Whether access-marker write traffic was required.
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96#[must_use]
97#[cfg_attr(docsrs, doc(cfg(feature = "access")))]
98pub enum AccessUpdate {
99    /// The marker timestamp was updated.
100    Recorded,
101    /// A recent marker made an update unnecessary.
102    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}