Skip to main content

coreshift_core/
fs.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Filesystem-oriented low-level helpers.
6//!
7//! This module contains lightweight Linux and Android file probes and helpers
8//! that are useful near the OS boundary, including path existence checks and
9//! page-cache read-ahead hints.
10
11use crate::CoreError;
12use std::os::unix::io::AsRawFd;
13use std::path::Path;
14use std::time::UNIX_EPOCH;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct PathFingerprint {
18    pub len: u64,
19    pub modified_ns: u128,
20}
21
22pub fn path_fingerprint(path: &Path) -> Result<PathFingerprint, CoreError> {
23    let metadata = std::fs::metadata(path).map_err(|err| {
24        CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "path_fingerprint")
25    })?;
26    let modified_ns = metadata
27        .modified()
28        .ok()
29        .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
30        .map(|duration| duration.as_nanos())
31        .unwrap_or_default();
32    Ok(PathFingerprint {
33        len: metadata.len(),
34        modified_ns,
35    })
36}
37
38/// Probe whether a filesystem path is accessible and exists.
39///
40/// NOTE: This follows symbolic links. It uses `libc::access` with `F_OK`
41/// so the check is a single syscall with no Rust allocator involvement.
42/// Returns `true` if the path is accessible or visible, `false` on any error
43/// (including `ENOENT`, `EACCES`, or invalid path bytes).
44pub fn path_exists(path: &str) -> bool {
45    match std::ffi::CString::new(path) {
46        Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
47        Err(_) => false,
48    }
49}
50
51/// Probe whether a path exists without following symbolic links.
52///
53/// Returns `true` if the path exists, including a dangling symlink.
54pub fn path_lstat_exists(path: &str) -> bool {
55    match std::ffi::CString::new(path) {
56        Ok(c) => unsafe {
57            let mut stat = std::mem::zeroed();
58            libc::lstat(c.as_ptr(), &mut stat) == 0
59        },
60        Err(_) => false,
61    }
62}
63
64/// Read a file into a string.
65///
66/// This stays as a small convenience helper for low-level modules that treat
67/// blocking filesystem or procfs reads as an acceptable boundary cost.
68pub fn read_to_string(path: &str) -> Result<String, CoreError> {
69    std::fs::read_to_string(path)
70        .map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
71}
72
73/// Advise the kernel to begin reading file data into the page cache.
74///
75/// This is an advisory hint only. It can help warm likely-needed file ranges,
76/// but the kernel may ignore the request, perform only part of it, or return
77/// before the data is fully resident in memory.
78///
79/// The `offset` and `len` identify the byte range to prefetch for `fd`.
80/// Success means the kernel accepted the request, not that subsequent reads
81/// are guaranteed to be cache hits.
82pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
83    readahead_raw(fd.as_raw_fd(), offset, len)
84}
85
86/// Map a file range, advise the kernel that it will be needed, then unmap it.
87///
88/// `offset` must be page-aligned. This low-level primitive rejects unaligned
89/// offsets with `EINVAL` instead of silently widening the requested range.
90pub fn mmap_madvise(
91    fd: impl AsRawFd,
92    offset: u64,
93    len: usize,
94    touch: bool,
95) -> Result<(), CoreError> {
96    mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
97}
98
99#[cfg(any(target_os = "linux", target_os = "android"))]
100fn mmap_madvise_raw(
101    fd: libc::c_int,
102    offset: u64,
103    len: usize,
104    touch: bool,
105) -> Result<(), CoreError> {
106    if len == 0 {
107        return Ok(());
108    }
109
110    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
111    if page_size <= 0 {
112        return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
113    }
114    let page_size = page_size as u64;
115    if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
116        return Err(CoreError::sys(libc::EINVAL, "mmap"));
117    }
118
119    let ptr = unsafe {
120        libc::mmap(
121            std::ptr::null_mut(),
122            len,
123            libc::PROT_READ,
124            libc::MAP_PRIVATE,
125            fd,
126            offset as libc::off_t,
127        )
128    };
129    if ptr == libc::MAP_FAILED {
130        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
131        return Err(CoreError::sys(code, "mmap"));
132    }
133
134    let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
135        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
136        Err(CoreError::sys(code, "madvise"))
137    } else {
138        if touch {
139            let mut pos = 0usize;
140            let page_size = page_size as usize;
141            while pos < len {
142                unsafe {
143                    std::ptr::read_volatile((ptr as *const u8).add(pos));
144                }
145                pos = pos.saturating_add(page_size);
146            }
147        }
148        Ok(())
149    };
150
151    if unsafe { libc::munmap(ptr, len) } == -1 {
152        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
153        return Err(CoreError::sys(code, "munmap"));
154    }
155    result
156}
157
158#[cfg(not(any(target_os = "linux", target_os = "android")))]
159fn mmap_madvise_raw(
160    _fd: libc::c_int,
161    _offset: u64,
162    _len: usize,
163    _touch: bool,
164) -> Result<(), CoreError> {
165    Err(CoreError::sys(libc::ENOSYS, "mmap"))
166}
167
168#[cfg(any(target_os = "linux", target_os = "android"))]
169fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
170    if offset > libc::off64_t::MAX as u64 {
171        return Err(CoreError::sys(libc::EINVAL, "readahead"));
172    }
173
174    let count = len as libc::size_t;
175    let offset = offset as libc::off64_t;
176
177    loop {
178        let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
179        if ret == -1 {
180            let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
181            if code == libc::EINTR {
182                continue;
183            }
184            return Err(CoreError::sys(code, "readahead"));
185        }
186        return Ok(());
187    }
188}
189
190#[cfg(not(any(target_os = "linux", target_os = "android")))]
191fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
192    Err(CoreError::sys(libc::ENOSYS, "readahead"))
193}
194
195#[cfg(target_os = "linux")]
196#[inline(always)]
197const fn readahead_syscall_number() -> libc::c_long {
198    libc::SYS_readahead
199}
200
201#[cfg(all(target_os = "android", target_arch = "aarch64"))]
202#[inline(always)]
203const fn readahead_syscall_number() -> libc::c_long {
204    213
205}
206
207#[cfg(all(target_os = "android", target_arch = "arm"))]
208#[inline(always)]
209const fn readahead_syscall_number() -> libc::c_long {
210    225
211}
212
213#[cfg(all(target_os = "android", target_arch = "x86_64"))]
214#[inline(always)]
215const fn readahead_syscall_number() -> libc::c_long {
216    187
217}
218
219#[cfg(all(target_os = "android", target_arch = "x86"))]
220#[inline(always)]
221const fn readahead_syscall_number() -> libc::c_long {
222    225
223}
224
225#[cfg(test)]
226mod tests {
227    #[cfg(target_os = "linux")]
228    #[test]
229    fn test_readahead_syscall_number_linux_matches_libc() {
230        assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
231    }
232
233    #[cfg(all(target_os = "android", target_arch = "aarch64"))]
234    #[test]
235    fn test_readahead_syscall_number_android_aarch64() {
236        assert_eq!(super::readahead_syscall_number(), 213);
237    }
238
239    #[cfg(all(target_os = "android", target_arch = "arm"))]
240    #[test]
241    fn test_readahead_syscall_number_android_arm() {
242        assert_eq!(super::readahead_syscall_number(), 225);
243    }
244
245    #[cfg(all(target_os = "android", target_arch = "x86_64"))]
246    #[test]
247    fn test_readahead_syscall_number_android_x86_64() {
248        assert_eq!(super::readahead_syscall_number(), 187);
249    }
250
251    #[cfg(all(target_os = "android", target_arch = "x86"))]
252    #[test]
253    fn test_readahead_syscall_number_android_x86() {
254        assert_eq!(super::readahead_syscall_number(), 225);
255    }
256}