Skip to main content

gsym_cache/
lookup.rs

1use std::ffi::{OsStr, OsString};
2use std::fmt;
3use std::fs::{File, Metadata};
4use std::io;
5use std::path::{Path, PathBuf};
6use std::sync::OnceLock;
7#[cfg(feature = "manage")]
8use std::sync::atomic::AtomicBool;
9
10use crate::error::io_error;
11use crate::{BuildId, Error, Result, layout};
12
13/// Version of the conversion policy used to create cached GSYM files.
14///
15/// Incrementing the epoch creates a namespace without reusing artifacts from
16/// an older conversion policy.
17#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
19pub struct CacheEpoch(u32);
20
21impl CacheEpoch {
22    /// Creates an epoch from its stable numeric value.
23    #[must_use]
24    pub const fn new(value: u32) -> Self {
25        Self(value)
26    }
27
28    /// Returns the numeric epoch.
29    #[must_use]
30    pub const fn get(self) -> u32 {
31        self.0
32    }
33}
34
35impl From<u32> for CacheEpoch {
36    fn from(value: u32) -> Self {
37        Self::new(value)
38    }
39}
40
41impl From<CacheEpoch> for u32 {
42    fn from(epoch: CacheEpoch) -> Self {
43        epoch.get()
44    }
45}
46
47impl fmt::Display for CacheEpoch {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        self.0.fmt(formatter)
50    }
51}
52
53/// A configured GSYM filesystem cache.
54///
55/// Directory descriptors are pinned once opened. Create a new `Cache` after
56/// replacing a cache namespace directory; ordinary entry creation and removal
57/// do not require reopening it.
58///
59/// See [`docs::deployment`](crate::docs::deployment) for root selection,
60/// epochs, filesystem requirements, and the trust model.
61#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
62pub struct Cache {
63    root: PathBuf,
64    base: PathBuf,
65    object_directory_path: PathBuf,
66    epoch: CacheEpoch,
67    identity: OnceLock<RootIdentity>,
68    object_directory: OnceLock<File>,
69    #[cfg(feature = "access")]
70    is_xdg: bool,
71    #[cfg(feature = "access")]
72    pub(crate) access_directory: OnceLock<File>,
73    #[cfg(feature = "manage")]
74    pub(crate) manage_prepared: AtomicBool,
75}
76
77impl Cache {
78    /// Opens a cache namespace without creating it.
79    ///
80    /// A missing root is valid and produces cache misses. An existing root
81    /// must be a private non-symlink directory owned by the effective user.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the existing root is insecure or cannot be
86    /// inspected.
87    pub fn open(root: impl AsRef<Path>, epoch: CacheEpoch) -> Result<Self> {
88        let supplied_root = root.as_ref();
89        let root = std::path::absolute(supplied_root)
90            .map_err(|source| io_error("resolve absolute cache root", supplied_root, source))?;
91        let identity = OnceLock::new();
92        if let Some(existing) = inspect_root(&root)? {
93            let _ = identity.set(existing);
94        }
95        let base = layout::namespace(&root, epoch);
96        let object_directory_path = base.join(layout::OBJECTS).join(layout::BUILD_ID);
97        Ok(Self {
98            base,
99            root,
100            object_directory_path,
101            epoch,
102            identity,
103            object_directory: OnceLock::new(),
104            #[cfg(feature = "access")]
105            is_xdg: false,
106            #[cfg(feature = "access")]
107            access_directory: OnceLock::new(),
108            #[cfg(feature = "manage")]
109            manage_prepared: AtomicBool::new(false),
110        })
111    }
112
113    /// Opens an application cache below `$XDG_CACHE_HOME`.
114    ///
115    /// An unset, empty, or relative `XDG_CACHE_HOME` falls back to
116    /// `$HOME/.cache` as required by the XDG Base Directory Specification.
117    /// `application` must be one normal path component. The resulting root is
118    /// `<cache-home>/<application>/gsym`.
119    ///
120    /// Privileged applications should use [`Cache::open`] with an explicitly
121    /// configured root instead of trusting process environment variables.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error when no absolute cache home is available, the
126    /// application identifier is invalid, or the resulting root is insecure.
127    pub fn open_xdg(application: impl AsRef<OsStr>, epoch: CacheEpoch) -> Result<Self> {
128        let root = xdg_cache_root(
129            application.as_ref(),
130            std::env::var_os("XDG_CACHE_HOME"),
131            std::env::var_os("HOME"),
132        )?;
133        validated_xdg_paths(&root)?;
134        let cache = Self::open(root, epoch)?;
135        #[cfg(feature = "access")]
136        {
137            let mut cache = cache;
138            cache.is_xdg = true;
139            Ok(cache)
140        }
141        #[cfg(not(feature = "access"))]
142        {
143            Ok(cache)
144        }
145    }
146
147    /// Returns the user-configured cache root.
148    #[must_use]
149    pub fn root(&self) -> &Path {
150        &self.root
151    }
152
153    /// Returns this cache's converter epoch.
154    #[must_use]
155    pub const fn epoch(&self) -> CacheEpoch {
156        self.epoch
157    }
158
159    /// Opens a cached GSYM file without taking a lock.
160    ///
161    /// The returned entry owns its read-only file descriptor, preventing a
162    /// lookup-to-open race with pruning. Lookup validates filesystem ownership
163    /// and file type but deliberately does not decode or fully verify GSYM on
164    /// this hot path. Managed population and scrubbing perform full verification.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error for filesystem failures or an untrusted entry. A file
169    /// that does not exist returns `Ok(None)`.
170    pub fn lookup(&self, build_id: &BuildId) -> Result<Option<CacheEntry>> {
171        let Some(directory) = self.object_directory()? else {
172            return Ok(None);
173        };
174        let key = layout::object_key(build_id);
175        let file = match open_read_only_at(directory, &key) {
176            Ok(file) => file,
177            Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
178            Err(source) => {
179                return Err(io_error(
180                    "open cache entry",
181                    layout::object(&self.base, build_id),
182                    source,
183                ));
184            }
185        };
186        let metadata = file.metadata().map_err(|source| {
187            io_error(
188                "inspect cache entry",
189                layout::object(&self.base, build_id),
190                source,
191            )
192        })?;
193        if !self.is_trusted_entry(&metadata)? {
194            return Err(Error::UntrustedEntry {
195                path: layout::object(&self.base, build_id),
196            });
197        }
198        Ok(Some(CacheEntry::new(file, metadata.len())))
199    }
200
201    fn object_directory(&self) -> Result<Option<&File>> {
202        if let Some(directory) = self.object_directory.get() {
203            return Ok(Some(directory));
204        }
205        // Keep repeated lookups against a cache that has never been created to
206        // one syscall; the secure component walk below remains authoritative.
207        match probe_directory(&self.object_directory_path) {
208            Ok(()) => {}
209            Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
210            Err(source) => {
211                return Err(io_error(
212                    "probe cache object directory",
213                    &self.object_directory_path,
214                    source,
215                ));
216            }
217        }
218        let Some(directory) = open_directory_chain(&self.object_directory_path, false)? else {
219            return Ok(None);
220        };
221        drop(self.object_directory.set(directory));
222        Ok(self.object_directory.get())
223    }
224
225    #[cfg(feature = "manage")]
226    pub(crate) fn lookup_path(&self, path: &Path) -> Result<Option<CacheEntry>> {
227        let file = match open_read_only(path) {
228            Ok(file) => file,
229            Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
230            Err(source) => return Err(io_error("open cache entry", path, source)),
231        };
232        let metadata = file
233            .metadata()
234            .map_err(|source| io_error("inspect cache entry", path, source))?;
235        self.validate_entry(path, &metadata)?;
236        Ok(Some(CacheEntry::new(file, metadata.len())))
237    }
238
239    #[cfg(feature = "access")]
240    pub(crate) fn base(&self) -> &Path {
241        &self.base
242    }
243
244    #[cfg(feature = "access")]
245    pub(crate) fn xdg_paths(&self) -> Option<(&Path, &Path)> {
246        if !self.is_xdg {
247            return None;
248        }
249        let application = self.root.parent()?;
250        Some((application.parent()?, application))
251    }
252
253    pub(crate) fn ensure_identity(&self) -> Result<&RootIdentity> {
254        if let Some(identity) = self.identity.get() {
255            return Ok(identity);
256        }
257        let identity = inspect_root(&self.root)?.ok_or_else(|| Error::InsecureDirectory {
258            path: self.root.clone(),
259        })?;
260        Ok(self.identity.get_or_init(|| identity))
261    }
262
263    fn is_trusted_entry(&self, metadata: &Metadata) -> Result<bool> {
264        Ok(metadata.is_file() && self.ensure_identity()?.owns(metadata))
265    }
266
267    #[cfg(feature = "access")]
268    pub(crate) fn validate_entry(&self, path: &Path, metadata: &Metadata) -> Result<()> {
269        if !self.is_trusted_entry(metadata)? {
270            return Err(Error::UntrustedEntry {
271                path: path.to_path_buf(),
272            });
273        }
274        Ok(())
275    }
276
277    #[cfg(feature = "access")]
278    pub(crate) fn validate_file(&self, path: &Path, file: &File) -> Result<()> {
279        let metadata = file
280            .metadata()
281            .map_err(|source| io_error("inspect cache file", path, source))?;
282        self.validate_entry(path, &metadata)
283    }
284
285    #[cfg(feature = "access")]
286    pub(crate) fn owns_uid(&self, uid: u32) -> Result<bool> {
287        Ok(self.ensure_identity()?.uid == uid)
288    }
289}
290
291fn validated_xdg_paths(root: &Path) -> Result<()> {
292    let application = root.parent().ok_or_else(|| Error::InsecureDirectory {
293        path: root.to_path_buf(),
294    })?;
295    let cache_home = application
296        .parent()
297        .ok_or_else(|| Error::InsecureDirectory {
298            path: root.to_path_buf(),
299        })?;
300    inspect_xdg_anchor(cache_home)?;
301    inspect_xdg_anchor_parent(cache_home)?;
302    inspect_xdg_anchor(application)?;
303    Ok(())
304}
305
306fn inspect_xdg_anchor(path: &Path) -> Result<()> {
307    let Some(directory) = open_directory_chain(path, false)? else {
308        return Ok(());
309    };
310    validate_xdg_anchor(path, &directory)
311}
312
313fn inspect_xdg_anchor_parent(path: &Path) -> Result<()> {
314    let parent = path.parent().ok_or_else(|| Error::InsecureDirectory {
315        path: path.to_path_buf(),
316    })?;
317    let directory =
318        open_directory_chain(parent, false)?.ok_or_else(|| Error::InsecureDirectory {
319            path: parent.to_path_buf(),
320        })?;
321    let metadata = directory
322        .metadata()
323        .map_err(|source| io_error("inspect XDG cache parent", parent, source))?;
324    if !is_replace_protected_directory(&metadata, rustix::process::geteuid().as_raw()) {
325        return Err(Error::InsecureDirectory {
326            path: parent.to_path_buf(),
327        });
328    }
329    Ok(())
330}
331
332impl fmt::Debug for Cache {
333    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
334        formatter
335            .debug_struct("Cache")
336            .field("root", &self.root)
337            .field("epoch", &self.epoch)
338            .finish_non_exhaustive()
339    }
340}
341
342/// An opened, immutable cache entry.
343#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
344pub struct CacheEntry {
345    file: File,
346    len: u64,
347}
348
349impl CacheEntry {
350    pub(crate) const fn new(file: File, len: u64) -> Self {
351        Self { file, len }
352    }
353
354    /// Borrows the read-only cached GSYM file.
355    #[must_use]
356    pub const fn file(&self) -> &File {
357        &self.file
358    }
359
360    /// Consumes the entry and returns its read-only file.
361    #[must_use]
362    pub fn into_file(self) -> File {
363        self.file
364    }
365
366    /// Returns the file length observed when the entry was opened.
367    #[must_use]
368    pub const fn len(&self) -> u64 {
369        self.len
370    }
371
372    /// Returns whether the entry is empty.
373    #[must_use]
374    pub const fn is_empty(&self) -> bool {
375        self.len == 0
376    }
377}
378
379impl AsRef<File> for CacheEntry {
380    fn as_ref(&self) -> &File {
381        self.file()
382    }
383}
384
385impl std::os::fd::AsFd for CacheEntry {
386    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
387        std::os::fd::AsFd::as_fd(&self.file)
388    }
389}
390
391impl From<CacheEntry> for File {
392    fn from(entry: CacheEntry) -> Self {
393        entry.into_file()
394    }
395}
396
397impl fmt::Debug for CacheEntry {
398    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
399        formatter
400            .debug_struct("CacheEntry")
401            .field("len", &self.len)
402            .finish_non_exhaustive()
403    }
404}
405
406#[derive(Clone, Copy, Debug)]
407pub(crate) struct RootIdentity {
408    uid: u32,
409}
410
411impl RootIdentity {
412    fn owns(self, metadata: &Metadata) -> bool {
413        use std::os::unix::fs::MetadataExt;
414        metadata.uid() == self.uid
415    }
416}
417
418fn inspect_root(path: &Path) -> Result<Option<RootIdentity>> {
419    let Some(directory) = open_directory_chain(path, false)? else {
420        return Ok(None);
421    };
422    let metadata = directory
423        .metadata()
424        .map_err(|source| io_error("inspect cache root", path, source))?;
425    if !is_owned_private_directory(&metadata, rustix::process::geteuid().as_raw()) {
426        return Err(Error::InsecureDirectory {
427            path: path.to_path_buf(),
428        });
429    }
430    Ok(Some(RootIdentity {
431        uid: metadata_uid(&metadata),
432    }))
433}
434
435#[cfg(feature = "access")]
436pub(crate) fn ensure_private_directory(path: &Path) -> Result<()> {
437    drop(open_private_directory(path)?);
438    Ok(())
439}
440
441#[cfg(feature = "access")]
442pub(crate) fn open_private_directory(path: &Path) -> Result<File> {
443    let directory = open_directory_chain(path, true)?.ok_or_else(|| Error::InsecureDirectory {
444        path: path.to_path_buf(),
445    })?;
446    let metadata = directory
447        .metadata()
448        .map_err(|source| io_error("inspect cache directory", path, source))?;
449    if !is_owned_private_directory(&metadata, rustix::process::geteuid().as_raw()) {
450        return Err(Error::InsecureDirectory {
451            path: path.to_path_buf(),
452        });
453    }
454    Ok(directory)
455}
456
457#[cfg(feature = "access")]
458pub(crate) fn ensure_xdg_anchor(path: &Path) -> Result<()> {
459    let directory = open_directory_chain(path, true)?.ok_or_else(|| Error::InsecureDirectory {
460        path: path.to_path_buf(),
461    })?;
462    validate_xdg_anchor(path, &directory)
463}
464
465fn validate_xdg_anchor(path: &Path, directory: &File) -> Result<()> {
466    let metadata = directory
467        .metadata()
468        .map_err(|source| io_error("inspect XDG cache home", path, source))?;
469    if !is_owned_secure_parent(&metadata, rustix::process::geteuid().as_raw()) {
470        return Err(Error::InsecureDirectory {
471            path: path.to_path_buf(),
472        });
473    }
474    Ok(())
475}
476
477fn is_owned_private_directory(metadata: &Metadata, uid: u32) -> bool {
478    metadata.is_dir() && metadata_uid(metadata) == uid && is_private(metadata)
479}
480
481fn is_owned_secure_parent(metadata: &Metadata, uid: u32) -> bool {
482    metadata.is_dir() && metadata_uid(metadata) == uid && !is_group_or_other_writable(metadata)
483}
484
485fn is_replace_protected_directory(metadata: &Metadata, uid: u32) -> bool {
486    use std::os::unix::fs::PermissionsExt as _;
487
488    let owner = metadata_uid(metadata);
489    is_replace_protected(owner, uid, metadata.permissions().mode(), metadata.is_dir())
490}
491
492const fn is_replace_protected(owner: u32, uid: u32, mode: u32, is_directory: bool) -> bool {
493    is_directory && (owner == uid || owner == 0) && (mode & 0o022 == 0 || mode & 0o1000 != 0)
494}
495
496fn open_directory_chain(path: &Path, create: bool) -> Result<Option<File>> {
497    use std::path::Component;
498
499    use rustix::fs::{Mode, OFlags};
500
501    if path
502        .components()
503        .any(|component| matches!(component, Component::ParentDir | Component::Prefix(_)))
504    {
505        return Err(Error::InsecureDirectory {
506            path: path.to_path_buf(),
507        });
508    }
509
510    let start = if path.is_absolute() { "/" } else { "." };
511    let mut directory = rustix::fs::open(
512        start,
513        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
514        Mode::empty(),
515    )
516    .map(File::from)
517    .map_err(|source| io_error("open cache path anchor", start, io::Error::from(source)))?;
518
519    for component in path.components() {
520        let Component::Normal(name) = component else {
521            if matches!(component, Component::RootDir | Component::CurDir) {
522                continue;
523            }
524            return Err(Error::InsecureDirectory {
525                path: path.to_path_buf(),
526            });
527        };
528        loop {
529            match rustix::fs::openat(
530                &directory,
531                name,
532                OFlags::RDONLY
533                    | OFlags::DIRECTORY
534                    | OFlags::CLOEXEC
535                    | OFlags::NOFOLLOW
536                    | OFlags::NONBLOCK,
537                Mode::empty(),
538            ) {
539                Ok(next) => {
540                    directory = File::from(next);
541                    break;
542                }
543                Err(source) if source == rustix::io::Errno::NOENT && !create => {
544                    return Ok(None);
545                }
546                Err(source) if source == rustix::io::Errno::NOENT && create => {
547                    match rustix::fs::mkdirat(
548                        &directory,
549                        name,
550                        Mode::RUSR | Mode::WUSR | Mode::XUSR,
551                    ) {
552                        Ok(()) => directory.sync_all().map_err(|source| {
553                            io_error("sync cache directory parent", path, source)
554                        })?,
555                        Err(source) if source == rustix::io::Errno::EXIST => {}
556                        Err(source) => {
557                            return Err(io_error(
558                                "create cache directory",
559                                path,
560                                io::Error::from(source),
561                            ));
562                        }
563                    }
564                }
565                Err(source) => {
566                    return Err(io_error(
567                        "open cache directory",
568                        path,
569                        io::Error::from(source),
570                    ));
571                }
572            }
573        }
574    }
575    Ok(Some(directory))
576}
577
578#[cfg(feature = "manage")]
579pub(crate) fn open_existing_directory(path: &Path) -> Result<File> {
580    open_directory_chain(path, false)?.ok_or_else(|| {
581        io_error(
582            "open cache directory",
583            path,
584            io::Error::from(io::ErrorKind::NotFound),
585        )
586    })
587}
588
589fn metadata_uid(metadata: &Metadata) -> u32 {
590    use std::os::unix::fs::MetadataExt;
591    metadata.uid()
592}
593
594#[expect(
595    clippy::verbose_bit_mask,
596    reason = "the Unix permission bits are clearer as their conventional octal mask"
597)]
598fn is_private(metadata: &Metadata) -> bool {
599    use std::os::unix::fs::PermissionsExt;
600    metadata.permissions().mode() & 0o077 == 0
601}
602
603fn is_group_or_other_writable(metadata: &Metadata) -> bool {
604    use std::os::unix::fs::PermissionsExt;
605    metadata.permissions().mode() & 0o022 != 0
606}
607
608#[cfg(feature = "manage")]
609pub(crate) fn open_read_only(path: &Path) -> io::Result<File> {
610    use rustix::fs::{Mode, OFlags};
611
612    rustix::fs::open(
613        path,
614        OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
615        Mode::empty(),
616    )
617    .map(File::from)
618    .map_err(io::Error::from)
619}
620
621fn open_read_only_at(directory: &File, key: &layout::Key) -> io::Result<File> {
622    use rustix::fs::{Mode, OFlags, ResolveFlags};
623
624    let flags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK;
625    let path = key
626        .as_c_str()
627        .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
628    match rustix::fs::openat2(
629        directory,
630        path,
631        flags,
632        Mode::empty(),
633        ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS,
634    ) {
635        Ok(file) => Ok(File::from(file)),
636        Err(rustix::io::Errno::NOSYS | rustix::io::Errno::PERM) => {
637            let shard_name = key
638                .shard()
639                .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
640            let filename = key
641                .filename()
642                .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
643            let shard = rustix::fs::openat(
644                directory,
645                shard_name,
646                OFlags::RDONLY
647                    | OFlags::DIRECTORY
648                    | OFlags::CLOEXEC
649                    | OFlags::NOFOLLOW
650                    | OFlags::NONBLOCK,
651                Mode::empty(),
652            )?;
653            rustix::fs::openat(&shard, filename, flags, Mode::empty())
654                .map(File::from)
655                .map_err(io::Error::from)
656        }
657        Err(source) => Err(io::Error::from(source)),
658    }
659}
660
661fn probe_directory(path: &Path) -> io::Result<()> {
662    use rustix::fs::{Mode, OFlags};
663
664    rustix::fs::open(
665        path,
666        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
667        Mode::empty(),
668    )
669    .map(drop)
670    .map_err(io::Error::from)
671}
672
673fn xdg_cache_root(
674    application: &OsStr,
675    xdg_cache_home: Option<OsString>,
676    home: Option<OsString>,
677) -> Result<PathBuf> {
678    let application_path = Path::new(application);
679    let mut components = application_path.components();
680    if !matches!(components.next(), Some(std::path::Component::Normal(_)))
681        || components.next().is_some()
682    {
683        return Err(Error::InvalidApplicationId {
684            value: application_path.to_path_buf(),
685        });
686    }
687    let base = xdg_cache_home
688        .filter(|value| !value.is_empty())
689        .map(PathBuf::from)
690        .filter(|path| path.is_absolute())
691        .or_else(|| {
692            home.filter(|value| !value.is_empty())
693                .map(PathBuf::from)
694                .filter(|path| path.is_absolute())
695                .map(|path| path.join(".cache"))
696        })
697        .ok_or(Error::CacheHomeUnavailable)?;
698    Ok(base.join(application_path).join("gsym"))
699}
700
701#[cfg(test)]
702mod tests {
703    use super::{Cache, CacheEpoch, is_replace_protected, validated_xdg_paths, xdg_cache_root};
704
705    #[test]
706    fn xdg_cache_root_uses_absolute_xdg_home_and_home_fallback() {
707        assert_eq!(
708            xdg_cache_root(
709                "chronon".as_ref(),
710                Some("/cache".into()),
711                Some("/home/u".into())
712            )
713            .expect("absolute XDG cache home is accepted"),
714            std::path::PathBuf::from("/cache/chronon/gsym")
715        );
716        assert_eq!(
717            xdg_cache_root(
718                "chronon".as_ref(),
719                Some("relative".into()),
720                Some("/home/u".into())
721            )
722            .expect("relative XDG cache home falls back to home"),
723            std::path::PathBuf::from("/home/u/.cache/chronon/gsym")
724        );
725    }
726
727    #[test]
728    fn xdg_cache_root_rejects_unsafe_or_unresolved_paths() {
729        assert!(xdg_cache_root("../chronon".as_ref(), Some("/cache".into()), None).is_err());
730        assert!(xdg_cache_root("chronon".as_ref(), None, Some("relative".into())).is_err());
731    }
732
733    #[test]
734    fn xdg_cache_anchor_must_not_be_shared_writable() {
735        use std::os::unix::fs::PermissionsExt as _;
736
737        let directory = tempfile::tempdir().expect("temporary directory is created");
738        std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
739            .expect("cache parent permissions are changed");
740        let anchor = directory.path().join("cache-home");
741        std::fs::create_dir(&anchor).expect("cache home is created");
742        std::fs::set_permissions(&anchor, std::fs::Permissions::from_mode(0o750))
743            .expect("cache home permissions are changed");
744        let result = validated_xdg_paths(&anchor.join("app/gsym"));
745        assert!(result.is_ok(), "{result:?}");
746
747        std::fs::set_permissions(&anchor, std::fs::Permissions::from_mode(0o770))
748            .expect("cache home permissions are changed");
749        assert!(validated_xdg_paths(&anchor.join("app/gsym")).is_err());
750
751        std::fs::set_permissions(&anchor, std::fs::Permissions::from_mode(0o700))
752            .expect("cache home permissions are changed");
753        std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o777))
754            .expect("cache parent permissions are changed");
755        assert!(validated_xdg_paths(&anchor.join("app/gsym")).is_err());
756        std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o1777))
757            .expect("cache parent permissions are changed");
758        let result = validated_xdg_paths(&anchor.join("app/gsym"));
759        assert!(result.is_ok(), "{result:?}");
760
761        std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
762            .expect("cache parent permissions are changed");
763        let application = anchor.join("app");
764        std::fs::create_dir(&application).expect("application cache is created");
765        std::fs::set_permissions(&application, std::fs::Permissions::from_mode(0o755))
766            .expect("application cache permissions are changed");
767        let result = validated_xdg_paths(&application.join("gsym"));
768        assert!(result.is_ok(), "{result:?}");
769
770        let uid = rustix::process::geteuid().as_raw();
771        assert!(!is_replace_protected(
772            uid.saturating_add(1),
773            uid,
774            0o1777,
775            true
776        ));
777    }
778
779    #[test]
780    fn cache_root_rejects_parent_components_before_creation() {
781        assert!(Cache::open("missing/../cache", CacheEpoch::new(1)).is_err());
782    }
783}