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(
126            fd.as_raw_fd(),
127            offset as libc::off_t,
128            len as libc::off_t,
129            advice,
130        )
131    };
132    if ret == 0 {
133        Ok(())
134    } else {
135        Err(CoreError::sys(ret, "posix_fadvise"))
136    }
137}
138
139/// Map a file range, advise the kernel that it will be needed, then unmap it.
140///
141/// `offset` must be page-aligned. This low-level primitive rejects unaligned
142/// offsets with `EINVAL` instead of silently widening the requested range.
143/// Map a file range and advise the kernel with `MADV_WILLNEED`.
144///
145/// ### Errors
146/// - `EBADF`: The file descriptor is invalid.
147/// - `EINVAL`: The offset is not page-aligned or the range is invalid.
148/// - `ENOMEM`: Insufficient kernel memory.
149pub fn mmap_madvise(
150    fd: impl AsRawFd,
151    offset: u64,
152    len: usize,
153    touch: bool,
154) -> Result<(), CoreError> {
155    mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
156}
157
158#[cfg(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    if len == 0 {
166        return Ok(());
167    }
168
169    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
170    if page_size <= 0 {
171        return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
172    }
173    let page_size = page_size as u64;
174    if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
175        return Err(CoreError::sys(libc::EINVAL, "mmap"));
176    }
177
178    let ptr = unsafe {
179        libc::mmap(
180            std::ptr::null_mut(),
181            len,
182            libc::PROT_READ,
183            libc::MAP_PRIVATE,
184            fd,
185            offset as libc::off_t,
186        )
187    };
188    if ptr == libc::MAP_FAILED {
189        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
190        return Err(CoreError::sys(code, "mmap"));
191    }
192
193    let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
194        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
195        Err(CoreError::sys(code, "madvise"))
196    } else {
197        if touch {
198            let mut pos = 0usize;
199            let page_size = page_size as usize;
200            while pos < len {
201                unsafe {
202                    std::ptr::read_volatile((ptr as *const u8).add(pos));
203                }
204                pos = pos.saturating_add(page_size);
205            }
206        }
207        Ok(())
208    };
209
210    if unsafe { libc::munmap(ptr, len) } == -1 {
211        let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
212        return Err(CoreError::sys(code, "munmap"));
213    }
214    result
215}
216
217#[cfg(not(any(target_os = "linux", target_os = "android")))]
218fn mmap_madvise_raw(
219    _fd: libc::c_int,
220    _offset: u64,
221    _len: usize,
222    _touch: bool,
223) -> Result<(), CoreError> {
224    Err(CoreError::sys(libc::ENOSYS, "mmap"))
225}
226
227#[cfg(any(target_os = "linux", target_os = "android"))]
228fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
229    if offset > libc::off64_t::MAX as u64 {
230        return Err(CoreError::sys(libc::EINVAL, "readahead"));
231    }
232
233    let count = len as libc::size_t;
234    let offset = offset as libc::off64_t;
235
236    loop {
237        let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
238        if ret == -1 {
239            let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
240            if code == libc::EINTR {
241                continue;
242            }
243            return Err(CoreError::sys(code, "readahead"));
244        }
245        return Ok(());
246    }
247}
248
249#[cfg(not(any(target_os = "linux", target_os = "android")))]
250fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
251    Err(CoreError::sys(libc::ENOSYS, "readahead"))
252}
253
254#[cfg(target_os = "linux")]
255#[inline(always)]
256const fn readahead_syscall_number() -> libc::c_long {
257    libc::SYS_readahead
258}
259
260#[cfg(all(target_os = "android", target_arch = "aarch64"))]
261#[inline(always)]
262const fn readahead_syscall_number() -> libc::c_long {
263    213
264}
265
266#[cfg(all(target_os = "android", target_arch = "arm"))]
267#[inline(always)]
268const fn readahead_syscall_number() -> libc::c_long {
269    225
270}
271
272#[cfg(all(target_os = "android", target_arch = "x86_64"))]
273#[inline(always)]
274const fn readahead_syscall_number() -> libc::c_long {
275    187
276}
277
278#[cfg(all(target_os = "android", target_arch = "x86"))]
279#[inline(always)]
280const fn readahead_syscall_number() -> libc::c_long {
281    225
282}
283
284#[cfg(test)]
285mod tests {
286    #[cfg(target_os = "linux")]
287    #[test]
288    fn test_readahead_syscall_number_linux_matches_libc() {
289        assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
290    }
291
292    #[cfg(all(target_os = "android", target_arch = "aarch64"))]
293    #[test]
294    fn test_readahead_syscall_number_android_aarch64() {
295        assert_eq!(super::readahead_syscall_number(), 213);
296    }
297
298    #[cfg(all(target_os = "android", target_arch = "arm"))]
299    #[test]
300    fn test_readahead_syscall_number_android_arm() {
301        assert_eq!(super::readahead_syscall_number(), 225);
302    }
303
304    #[cfg(all(target_os = "android", target_arch = "x86_64"))]
305    #[test]
306    fn test_readahead_syscall_number_android_x86_64() {
307        assert_eq!(super::readahead_syscall_number(), 187);
308    }
309
310    #[cfg(all(target_os = "android", target_arch = "x86"))]
311    #[test]
312    fn test_readahead_syscall_number_android_x86() {
313        assert_eq!(super::readahead_syscall_number(), 225);
314    }
315}