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
22/// Return a fingerprint of the file metadata at the specified path.
23///
24/// ### Errors
25/// - `EACCES`: Permission denied.
26/// - `ENOENT`: The path does not exist.
27pub fn path_fingerprint(path: &Path) -> Result<PathFingerprint, CoreError> {
28    let metadata = std::fs::metadata(path).map_err(|err| {
29        CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "path_fingerprint")
30    })?;
31    let modified_ns = metadata
32        .modified()
33        .ok()
34        .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
35        .map(|duration| duration.as_nanos())
36        .unwrap_or_default();
37    Ok(PathFingerprint {
38        len: metadata.len(),
39        modified_ns,
40    })
41}
42
43/// Probe whether a filesystem path is accessible and exists.
44///
45/// NOTE: This follows symbolic links. It uses `libc::access` with `F_OK`
46/// so the check is a single syscall with no Rust allocator involvement.
47/// Returns `true` if the path is accessible or visible, `false` on any error
48/// (including `ENOENT`, `EACCES`, or invalid path bytes).
49pub fn path_exists(path: &str) -> bool {
50    match std::ffi::CString::new(path) {
51        Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
52        Err(_) => false,
53    }
54}
55
56/// Probe whether a path exists without following symbolic links.
57///
58/// Returns `true` if the path exists, including a dangling symlink.
59pub fn path_lstat_exists(path: &str) -> bool {
60    match std::ffi::CString::new(path) {
61        Ok(c) => unsafe {
62            let mut stat = std::mem::zeroed();
63            libc::lstat(c.as_ptr(), &mut stat) == 0
64        },
65        Err(_) => false,
66    }
67}
68
69/// Read a file into a string.
70///
71/// This stays as a small convenience helper for low-level modules that treat
72/// blocking filesystem or procfs reads as an acceptable boundary cost.
73/// Read the entire contents of a file into a string.
74///
75/// ### Errors
76/// - `EACCES`: Permission denied.
77/// - `ENOENT`: The path does not exist.
78/// - `EIO`: Low-level I/O error.
79pub fn read_to_string(path: &str) -> Result<String, CoreError> {
80    std::fs::read_to_string(path)
81        .map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
82}
83
84/// Advise the kernel to begin reading file data into the page cache.
85///
86/// This is an advisory hint only. It can help warm likely-needed file ranges,
87/// but the kernel may ignore the request, perform only part of it, or return
88/// before the data is fully resident in memory.
89///
90/// The `offset` and `len` identify the byte range to prefetch for `fd`.
91/// Success means the kernel accepted the request, not that subsequent reads
92/// are guaranteed to be cache hits.
93/// Advise the kernel to begin reading file data into the page cache.
94///
95/// ### Errors
96/// - `EBADF`: The file descriptor is invalid.
97/// - `EINVAL`: The offset or length is invalid.
98pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
99    readahead_raw(fd.as_raw_fd(), offset, len)
100}
101
102// ── fadvise ──────────────────────────────────────────────────────────────────
103
104pub const FADV_NORMAL:     i32 = libc::POSIX_FADV_NORMAL;
105pub const FADV_RANDOM:     i32 = libc::POSIX_FADV_RANDOM;
106pub const FADV_SEQUENTIAL: i32 = libc::POSIX_FADV_SEQUENTIAL;
107pub const FADV_WILLNEED:   i32 = libc::POSIX_FADV_WILLNEED;
108pub const FADV_DONTNEED:   i32 = libc::POSIX_FADV_DONTNEED;
109pub const FADV_NOREUSE:    i32 = libc::POSIX_FADV_NOREUSE;
110
111/// Advise the kernel on the expected access pattern for a file range.
112///
113/// `offset` and `len` define the byte range; `len = 0` means "to end of file".
114/// `advice` is one of the `FADV_*` constants.
115///
116/// Unlike most syscalls, `posix_fadvise` returns the error code directly
117/// rather than setting `errno`.
118///
119/// ### Errors
120/// - `EBADF`: invalid file descriptor.
121/// - `EINVAL`: invalid advice value or unsupported `len`.
122/// - `ESPIPE`: the fd refers to a pipe.
123pub fn fadvise(fd: impl AsRawFd, offset: u64, len: usize, advice: i32) -> Result<(), CoreError> {
124    let ret = unsafe {
125        libc::posix_fadvise(fd.as_raw_fd(), offset as libc::off_t, len as libc::off_t, advice)
126    };
127    if ret == 0 { Ok(()) } else { Err(CoreError::sys(ret, "posix_fadvise")) }
128}
129
130/// Map a file range, advise the kernel that it will be needed, then unmap it.
131///
132/// `offset` must be page-aligned. This low-level primitive rejects unaligned
133/// offsets with `EINVAL` instead of silently widening the requested range.
134/// Map a file range and advise the kernel with `MADV_WILLNEED`.
135///
136/// ### Errors
137/// - `EBADF`: The file descriptor is invalid.
138/// - `EINVAL`: The offset is not page-aligned or the range is invalid.
139/// - `ENOMEM`: Insufficient kernel memory.
140pub fn mmap_madvise(
141    fd: impl AsRawFd,
142    offset: u64,
143    len: usize,
144    touch: bool,
145) -> Result<(), CoreError> {
146    mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
147}
148
149#[cfg(any(target_os = "linux", target_os = "android"))]
150fn mmap_madvise_raw(
151    fd: libc::c_int,
152    offset: u64,
153    len: usize,
154    touch: bool,
155) -> Result<(), CoreError> {
156    if len == 0 {
157        return Ok(());
158    }
159
160    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
161    if page_size <= 0 {
162        return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
163    }
164    let page_size = page_size as u64;
165    if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
166        return Err(CoreError::sys(libc::EINVAL, "mmap"));
167    }
168
169    let ptr = unsafe {
170        libc::mmap(
171            std::ptr::null_mut(),
172            len,
173            libc::PROT_READ,
174            libc::MAP_PRIVATE,
175            fd,
176            offset as libc::off_t,
177        )
178    };
179    if ptr == libc::MAP_FAILED {
180        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
181        return Err(CoreError::sys(code, "mmap"));
182    }
183
184    let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
185        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
186        Err(CoreError::sys(code, "madvise"))
187    } else {
188        if touch {
189            let mut pos = 0usize;
190            let page_size = page_size as usize;
191            while pos < len {
192                unsafe {
193                    std::ptr::read_volatile((ptr as *const u8).add(pos));
194                }
195                pos = pos.saturating_add(page_size);
196            }
197        }
198        Ok(())
199    };
200
201    if unsafe { libc::munmap(ptr, len) } == -1 {
202        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
203        return Err(CoreError::sys(code, "munmap"));
204    }
205    result
206}
207
208#[cfg(not(any(target_os = "linux", target_os = "android")))]
209fn mmap_madvise_raw(
210    _fd: libc::c_int,
211    _offset: u64,
212    _len: usize,
213    _touch: bool,
214) -> Result<(), CoreError> {
215    Err(CoreError::sys(libc::ENOSYS, "mmap"))
216}
217
218#[cfg(any(target_os = "linux", target_os = "android"))]
219fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
220    if offset > libc::off64_t::MAX as u64 {
221        return Err(CoreError::sys(libc::EINVAL, "readahead"));
222    }
223
224    let count = len as libc::size_t;
225    let offset = offset as libc::off64_t;
226
227    loop {
228        let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
229        if ret == -1 {
230            let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
231            if code == libc::EINTR {
232                continue;
233            }
234            return Err(CoreError::sys(code, "readahead"));
235        }
236        return Ok(());
237    }
238}
239
240#[cfg(not(any(target_os = "linux", target_os = "android")))]
241fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
242    Err(CoreError::sys(libc::ENOSYS, "readahead"))
243}
244
245#[cfg(target_os = "linux")]
246#[inline(always)]
247const fn readahead_syscall_number() -> libc::c_long {
248    libc::SYS_readahead
249}
250
251#[cfg(all(target_os = "android", target_arch = "aarch64"))]
252#[inline(always)]
253const fn readahead_syscall_number() -> libc::c_long {
254    213
255}
256
257#[cfg(all(target_os = "android", target_arch = "arm"))]
258#[inline(always)]
259const fn readahead_syscall_number() -> libc::c_long {
260    225
261}
262
263#[cfg(all(target_os = "android", target_arch = "x86_64"))]
264#[inline(always)]
265const fn readahead_syscall_number() -> libc::c_long {
266    187
267}
268
269#[cfg(all(target_os = "android", target_arch = "x86"))]
270#[inline(always)]
271const fn readahead_syscall_number() -> libc::c_long {
272    225
273}
274
275#[cfg(test)]
276mod tests {
277    #[cfg(target_os = "linux")]
278    #[test]
279    fn test_readahead_syscall_number_linux_matches_libc() {
280        assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
281    }
282
283    #[cfg(all(target_os = "android", target_arch = "aarch64"))]
284    #[test]
285    fn test_readahead_syscall_number_android_aarch64() {
286        assert_eq!(super::readahead_syscall_number(), 213);
287    }
288
289    #[cfg(all(target_os = "android", target_arch = "arm"))]
290    #[test]
291    fn test_readahead_syscall_number_android_arm() {
292        assert_eq!(super::readahead_syscall_number(), 225);
293    }
294
295    #[cfg(all(target_os = "android", target_arch = "x86_64"))]
296    #[test]
297    fn test_readahead_syscall_number_android_x86_64() {
298        assert_eq!(super::readahead_syscall_number(), 187);
299    }
300
301    #[cfg(all(target_os = "android", target_arch = "x86"))]
302    #[test]
303    fn test_readahead_syscall_number_android_x86() {
304        assert_eq!(super::readahead_syscall_number(), 225);
305    }
306}