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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
mod abi;
mod async_io;
mod content_store;
pub mod manager;
mod size_store;
pub mod store;
mod tree_store;
use std::{ffi::OsStr, sync::Arc, time::Duration};
pub use manager::DicfuseManager;
use crate::util::config;
/// Compute the backing store directory for a given base path.
///
/// - Global (root) view uses the configured `store_root` directly.
/// - Subdirectory stores are under "{store_root}/dicfuse/{sha256(base_path)[:16]}".
pub(crate) fn compute_store_dir_for_base_path_with_store_root(
store_root: &str,
base_path: &str,
) -> String {
let normalized = if base_path.is_empty() || base_path == "/" {
"/".to_string()
} else {
base_path.trim_end_matches('/').to_string()
};
if normalized == "/" {
store_root.to_string()
} else {
let digest = ring::digest::digest(&ring::digest::SHA256, normalized.as_bytes());
let hex = hex::encode(digest.as_ref());
format!("{}/dicfuse/{}", store_root, &hex[..16])
}
}
use async_trait::async_trait;
use libfuse_fs::{
context::OperationContext,
unionfs::{layer::Layer, Inode},
};
use rfuse3::{
raw::reply::{ReplyCreated, ReplyEntry},
Result,
};
use store::DictionaryStore;
use tree_store::StorageItem;
pub struct Dicfuse {
readable: bool,
pub store: Arc<DictionaryStore>,
}
unsafe impl Sync for Dicfuse {}
unsafe impl Send for Dicfuse {}
#[async_trait]
impl Layer for Dicfuse {
fn root_inode(&self) -> Inode {
1
}
/// Create a file in the layer (not supported for read-only Dicfuse).
/// This is called by OverlayFs during copy-up operations.
async fn create_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_mode: u32,
_flags: u32,
) -> Result<ReplyCreated> {
// Dicfuse is a read-only layer, does not support file creation
tracing::warn!(
"[{}:{}] create_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
/// Create a directory in the layer (not supported for read-only Dicfuse).
/// This is called by OverlayFs during copy-up operations.
async fn mkdir_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_mode: u32,
_umask: u32,
) -> Result<ReplyEntry> {
// Dicfuse is a read-only layer, does not support directory creation
tracing::warn!(
"[{}:{}] mkdir_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
/// Create a symlink in the layer (not supported for read-only Dicfuse).
/// This is called by OverlayFs during copy-up operations.
async fn symlink_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_link: &OsStr,
) -> Result<ReplyEntry> {
// Dicfuse is a read-only layer, does not support symlink creation
tracing::warn!(
"[{}:{}] symlink_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
/// Retrieve metadata with optional ID mapping control.
///
/// For Dicfuse (a virtual read-only layer), we ignore the `mapping` flag and
/// construct a synthetic `stat64` from our in-memory `StorageItem`, similar
/// to the old `do_getattr_helper` behavior in earlier libfuse-fs versions.
async fn getattr_with_mapping(
&self,
inode: Inode,
_handle: Option<u64>,
_mapping: bool,
) -> std::io::Result<(libc::stat64, std::time::Duration)> {
// Resolve inode -> StorageItem to derive type/size.
let item = self
.store
.get_inode(inode)
.await
.map_err(|_| std::io::Error::from_raw_os_error(libc::ENOENT))?;
// Use existing ReplyEntry metadata to stay consistent with other Dicfuse paths.
let attr = item.get_stat().attr;
let size: i64 = if item.is_dir() {
0
} else {
self.store
.get_or_fetch_file_size(inode, &item.hash)
.await
.min(i64::MAX as u64) as i64
};
let type_bits: libc::mode_t = match attr.kind {
rfuse3::FileType::Directory => libc::S_IFDIR,
rfuse3::FileType::Symlink => libc::S_IFLNK,
_ => libc::S_IFREG,
};
let perm: libc::mode_t = if item.is_dir() {
attr.perm as libc::mode_t
} else if self.store.is_executable(inode) {
0o755
} else {
0o644
};
let mode: libc::mode_t = type_bits | perm;
let nlink = if attr.nlink > 0 {
attr.nlink
} else if item.is_dir() {
2
} else {
1
};
// Construct stat64 structure using zeroed() for platform-specific padding fields.
let mut stat: libc::stat64 = unsafe { std::mem::zeroed() };
stat.st_dev = 0;
stat.st_ino = inode;
stat.st_nlink = nlink as _;
stat.st_mode = mode;
stat.st_uid = attr.uid;
stat.st_gid = attr.gid;
stat.st_rdev = 0;
stat.st_size = size;
stat.st_blksize = 4096;
stat.st_blocks = (size + 511) / 512; // Round up to 512-byte blocks
stat.st_atime = attr.atime.sec;
stat.st_atime_nsec = attr.atime.nsec.into();
stat.st_mtime = attr.mtime.sec;
stat.st_mtime_nsec = attr.mtime.nsec.into();
stat.st_ctime = attr.ctime.sec;
stat.st_ctime_nsec = attr.ctime.nsec.into();
Ok((stat, self.reply_ttl()))
}
}
#[allow(unused)]
impl Dicfuse {
pub async fn new() -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new().await.into(), // Assuming DictionaryStore has a new() method
}
}
/// Start the background import task (directory tree loading + depth-based content prefetch).
///
/// This should be called as early as possible (e.g., from antares service or DicfuseManager)
/// instead of waiting for the FUSE `init()` callback. The call is idempotent: only the first
/// invocation actually spawns the import task.
pub fn start_import(&self) {
if self.store.try_start_import() {
let s = self.store.clone();
tokio::spawn(async move {
store::import_arc(s).await;
});
}
}
pub async fn new_with_store_path(store_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_store_path(store_path)
.await
.into(),
}
}
/// Create a new Dicfuse instance with a base path and an explicit store path.
///
/// This is useful for Antares build scenarios where multiple mounts may coexist and we want
/// to isolate on-disk caches to avoid sled DB lock conflicts.
pub async fn new_with_base_path_and_store_path(base_path: &str, store_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_base_path_and_store_path(base_path, store_path)
.await
.into(),
}
}
/// Create a new Dicfuse instance with a base path for subdirectory mounting.
///
/// When `base_path` is set (e.g., "/third-party/mega"), the filesystem will:
/// - Only expose content under the specified path
/// - Remap paths so the base_path becomes the root "/"
///
/// This is useful for Antares build scenarios where only a specific
/// subdirectory of the monorepo is needed for a build task.
///
/// # Arguments
/// * `base_path` - The subdirectory path to use as root (e.g., "/third-party/mega")
///
/// # Example
/// ```ignore
/// let dicfuse = Dicfuse::new_with_base_path("/third-party/mega").await;
/// // Accessing "/" in this dicfuse actually accesses "/third-party/mega" in the monorepo
/// ```
pub async fn new_with_base_path(base_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_base_path(base_path).await.into(),
}
}
/// Get the base path of this Dicfuse instance.
///
/// Returns an empty string if no base path is set (full monorepo access).
pub fn base_path(&self) -> &str {
self.store.base_path()
}
pub(crate) fn reply_ttl(&self) -> Duration {
let is_subdir_mount = !(self.base_path().is_empty() || self.base_path() == "/");
let ttl_secs = if is_subdir_mount {
config::antares_dicfuse_reply_ttl_secs()
} else {
config::dicfuse_reply_ttl_secs()
};
Duration::from_secs(ttl_secs)
}
pub async fn get_stat(&self, item: StorageItem) -> ReplyEntry {
let mut e = item.get_stat();
e.ttl = self.reply_ttl();
if item.is_dir() {
e.attr.size = 0;
return e;
}
let size = self
.store
.file_size_for_stat(item.get_inode(), &item.hash)
.await;
e.attr.size = size;
e
}
/// Fast stat helper for hot paths (e.g., readdirplus): avoids any network IO.
/// Size will be taken from persisted size metadata if present; otherwise it may be 0 until
/// a later getattr/open triggers size discovery.
pub async fn get_stat_fast(&self, item: StorageItem) -> ReplyEntry {
let mut e = item.get_stat();
e.ttl = self.reply_ttl();
if item.is_dir() {
e.attr.size = 0;
return e;
}
e.attr.size = self.store.get_persisted_size(item.get_inode()).unwrap_or(0);
e
}
}
#[cfg(test)]
mod tests {
use std::{ffi::OsStr, path::PathBuf};
use libfuse_fs::unionfs::layer::Layer;
use tokio::signal;
use crate::dicfuse::Dicfuse;
#[tokio::test]
#[ignore = "manual test requiring root privileges for FUSE mount"]
async fn test_mount_dic() {
// Use environment variable or default to temp directory
let mount_path =
std::env::var("DIC_MOUNT_PATH").unwrap_or_else(|_| "/tmp/test_dic_mount".to_string());
// Create mount directory if it doesn't exist
std::fs::create_dir_all(&mount_path).expect("Failed to create mount directory");
let fs = Dicfuse::new().await;
let mountpoint = OsStr::new(&mount_path);
let mut mount_handle = crate::server::mount_filesystem(fs, mountpoint).await;
let handle = &mut mount_handle;
tokio::select! {
res = handle => res.unwrap(),
_ = signal::ctrl_c() => {
mount_handle.unmount().await.unwrap()
}
}
}
#[tokio::test]
async fn test_getattr_with_mapping_preserves_mode_and_size() {
let base = PathBuf::from("/tmp/dicfuse_attr_test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let dic = Dicfuse::new_with_store_path(base.to_str().unwrap()).await;
// Insert root and one file; mark file executable and with known content length.
dic.store.insert_mock_item(1, 0, "", true).await;
dic.store.insert_mock_item(2, 1, "file", false).await;
dic.store.save_file(2, b"abc".to_vec());
dic.store.set_executable(2, true);
let (file_stat, _) = dic.getattr_with_mapping(2, None, false).await.unwrap();
assert_eq!(file_stat.st_mode & libc::S_IFMT, libc::S_IFREG);
assert_eq!(file_stat.st_mode & 0o777, 0o755);
assert_eq!(file_stat.st_size, 3);
let (dir_stat, _) = dic.getattr_with_mapping(1, None, false).await.unwrap();
assert_eq!(dir_stat.st_mode & libc::S_IFMT, libc::S_IFDIR);
assert_eq!(dir_stat.st_mode & 0o777, 0o755);
assert_eq!(dir_stat.st_nlink, 2);
let _ = std::fs::remove_dir_all(&base);
}
}