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