1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025-present, fjall-rs
// Copyright (c) 2026-present, Dmitry Prudnikov
use crate::GlobalTableId;
use crate::descriptor_table::DescriptorTable;
use alloc::sync::Arc;
use crate::fs::{Fs, FsFile, FsOpenOptions};
use crate::path::Path;
/// Allows accessing a file (either cached or pinned)
#[derive(Clone)]
pub enum FileAccessor {
/// Pinned file descriptor
///
/// This is used in case file descriptor cache is `None` (to skip cache lookups)
File(Arc<dyn FsFile>),
/// Access to file descriptor cache with [`Fs`]-based fallback for
/// cache misses.
DescriptorTable {
/// The FD cache.
table: Arc<DescriptorTable>,
/// Filesystem backend for opening files on cache miss.
fs: Arc<dyn Fs>,
},
/// Sentinel used during [`Drop`] to move ownership of the file handle
/// before deleting the underlying file. Not constructed outside `Drop`.
#[doc(hidden)]
Closed,
}
impl FileAccessor {
#[must_use]
pub fn as_descriptor_table(&self) -> Option<&DescriptorTable> {
match self {
Self::DescriptorTable { table, .. } => Some(table),
Self::File(_) | Self::Closed => None,
}
}
/// Whether later accesses can be RETARGETED at a replacement inode:
/// the descriptor-table variant re-resolves the path after a cache
/// eviction, while a pinned FD is bound to its inode for the table's
/// lifetime (the in-place heal's unshare must refuse the detach then —
/// a rename would leave every later read on the dead inode).
#[must_use]
pub fn can_retarget(&self) -> bool {
matches!(self, Self::DescriptorTable { .. })
}
/// Returns a table FD, opening via [`Fs`] on descriptor-table cache miss.
///
/// Returns `(fd, None)` for pinned FDs (no cache involved),
/// `(fd, Some(true))` for descriptor-table cache hit,
/// `(fd, Some(false))` for cache miss (freshly opened and cached).
pub fn get_or_open_table(
&self,
table_id: &GlobalTableId,
path: &Path,
) -> crate::io::Result<(Arc<dyn FsFile>, Option<bool>)> {
match self {
Self::File(fd) => Ok((fd.clone(), None)),
Self::DescriptorTable { table, fs } => {
if let Some(fd) = table.access_for_table(table_id) {
return Ok((fd, Some(true)));
}
let fd: Arc<dyn FsFile> =
Arc::from(fs.open(path, &FsOpenOptions::new().read(true))?);
table.insert_for_table(*table_id, fd.clone());
Ok((fd, Some(false)))
}
Self::Closed => Err(crate::io::Error::other("file accessor closed")),
}
}
/// Returns a blob file FD, opening via [`Fs`] on descriptor-table cache miss.
///
/// See [`get_or_open_table`](Self::get_or_open_table) for
/// semantics of the `Option<bool>` cache-hit indicator.
pub fn get_or_open_blob_file(
&self,
table_id: &GlobalTableId,
path: &Path,
) -> crate::io::Result<(Arc<dyn FsFile>, Option<bool>)> {
match self {
Self::File(fd) => Ok((fd.clone(), None)),
Self::DescriptorTable { table, fs } => {
if let Some(fd) = table.access_for_blob_file(table_id) {
return Ok((fd, Some(true)));
}
let fd: Arc<dyn FsFile> =
Arc::from(fs.open(path, &FsOpenOptions::new().read(true))?);
table.insert_for_blob_file(*table_id, fd.clone());
Ok((fd, Some(false)))
}
Self::Closed => Err(crate::io::Error::other("file accessor closed")),
}
}
/// Pre-populates the blob file FD cache after creating a new blob file.
pub fn insert_for_blob_file(&self, table_id: GlobalTableId, fd: Arc<dyn FsFile>) {
if let Self::DescriptorTable { table, .. } = self {
table.insert_for_blob_file(table_id, fd);
}
}
/// Removes a table FD from the descriptor cache.
///
/// No-op for pinned `Self::File` variant (no cache to evict from).
pub fn remove_for_table(&self, table_id: &GlobalTableId) {
if let Self::DescriptorTable { table, .. } = self {
table.remove_for_table(table_id);
}
}
/// Removes a blob file FD from the descriptor cache.
///
/// No-op for pinned `Self::File` variant (no cache to evict from).
pub fn remove_for_blob_file(&self, table_id: &GlobalTableId) {
if let Self::DescriptorTable { table, .. } = self {
table.remove_for_blob_file(table_id);
}
}
}
impl core::fmt::Debug for FileAccessor {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Self::File(_) => write!(f, "FileAccessor::Pinned"),
Self::DescriptorTable { .. } => {
write!(f, "FileAccessor::DescriptorTable")
}
Self::Closed => write!(f, "FileAccessor::Closed"),
}
}
}