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#[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 #[must_use]
24 pub const fn new(value: u32) -> Self {
25 Self(value)
26 }
27
28 #[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#[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 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 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 #[must_use]
149 pub fn root(&self) -> &Path {
150 &self.root
151 }
152
153 #[must_use]
155 pub const fn epoch(&self) -> CacheEpoch {
156 self.epoch
157 }
158
159 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 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#[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 #[must_use]
356 pub const fn file(&self) -> &File {
357 &self.file
358 }
359
360 #[must_use]
362 pub fn into_file(self) -> File {
363 self.file
364 }
365
366 #[must_use]
368 pub const fn len(&self) -> u64 {
369 self.len
370 }
371
372 #[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}