Skip to main content

sweepx_platform_linux/
lib.rs

1// The Linux backend below speaks directly to Linux kernel interfaces (`openat2`,
2// `statx`, `fdopendir`/`readdir`). Those items do not exist in `libc` on other
3// targets, so the whole backend is gated on `target_os = "linux"` and non-Linux
4// hosts get the fail-closed stub at the end of this file. This keeps a
5// `--workspace` build honest on Windows and macOS instead of failing to compile.
6#[cfg(all(test, target_os = "linux"))]
7use std::cell::RefCell;
8#[cfg(target_os = "linux")]
9use std::ffi::{CStr, CString};
10#[cfg(target_os = "linux")]
11use std::io;
12#[cfg(target_os = "linux")]
13use std::mem::{self, MaybeUninit};
14#[cfg(target_os = "linux")]
15use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
16#[cfg(target_os = "linux")]
17use std::os::unix::ffi::OsStrExt;
18#[cfg(target_os = "linux")]
19use std::path::{Path, PathBuf};
20
21#[cfg(target_os = "linux")]
22use sweepx_model::{DecimalU128, NativeName, ReasonCode};
23#[cfg(target_os = "linux")]
24use sweepx_platform::{
25    BoundaryKind, BoundaryRecord, BoundedRegularFileReadError, BoundedRegularFileReadRequest,
26    EntryIdentity, EntryKind, ErrorRecord, FilesystemIdentity, HardLinkKey, MountIdentity,
27    OpenedDirectory, PresentRegularFileRead, RegularFileChangeStamp, RegularFileIdentityMismatch,
28    RegularFileMountMismatch, RegularFileObservation, RegularFileReadExpectation,
29    error_kind_for_io, fingerprint_for, known_count, known_u128, reason_for_io,
30};
31use sweepx_platform::{
32    CancellationToken, DirectoryEntryBatch, DirectoryEntryRecord, DirectoryHandleAdmission,
33    DirectoryReadLimits, EntryMetadata, PlatformError, PlatformScanner, RootAdmission, ScanRoot,
34    WalkEntry,
35};
36
37// Native mutation remains a test-only qualification concern. In particular,
38// this module is absent from normal and all-features library builds.
39#[cfg(all(test, target_os = "linux"))]
40mod trash_qualification;
41
42#[derive(Debug, Default, Clone)]
43pub struct LinuxPlatformScanner;
44
45impl LinuxPlatformScanner {
46    pub fn new() -> Self {
47        Self
48    }
49}
50
51/// Placeholder directory handle for hosts without the Linux backend.
52///
53/// It carries no descriptor because no traversal authority can exist here; every
54/// [`PlatformScanner`] method on a non-Linux host reports `Unsupported` instead.
55#[cfg(not(target_os = "linux"))]
56#[derive(Debug)]
57pub struct LinuxUnavailableDirectory;
58
59/// An owned, scan-scoped capability for one admitted directory.
60///
61/// The descriptor is intentionally not cloneable: the scanner frontier is the
62/// sole owner of traversal authority. `display_path` is reporting data only and
63/// is never used to reopen the directory.
64#[cfg(target_os = "linux")]
65#[derive(Debug)]
66pub struct LinuxDirectoryHandle {
67    fd: OwnedFd,
68    display_path: PathBuf,
69    cursor: DirectoryCursor,
70}
71
72#[cfg(target_os = "linux")]
73#[derive(Debug)]
74enum DirectoryCursor {
75    NotStarted,
76    Active {
77        stream: DirectoryStream,
78        pending: Option<DirectoryEntryRecord>,
79    },
80}
81
82#[cfg(target_os = "linux")]
83#[derive(Debug)]
84struct DirectoryStream(*mut libc::DIR);
85
86// SAFETY: a stream lives in exactly one non-Clone directory handle. The handle
87// is movable between threads, but enumeration requires exclusive `&mut` access
88// and the DIR* is never used concurrently or exposed outside this module.
89#[cfg(target_os = "linux")]
90unsafe impl Send for DirectoryStream {}
91
92#[cfg(target_os = "linux")]
93impl Drop for DirectoryStream {
94    fn drop(&mut self) {
95        // SAFETY: the non-null stream was returned by `fdopendir` and ownership
96        // was transferred to this guard.
97        unsafe {
98            libc::closedir(self.0);
99        }
100    }
101}
102
103#[cfg(target_os = "linux")]
104impl LinuxPlatformScanner {
105    const REGULAR_FILE_READ_CHUNK_BYTES: usize = 8192;
106
107    fn ensure_not_cancelled(cancel: &CancellationToken) -> Result<(), PlatformError> {
108        if cancel.is_cancelled() {
109            return Err(PlatformError::Cancelled);
110        }
111        Ok(())
112    }
113
114    fn ensure_regular_file_read_not_cancelled(
115        cancel: &CancellationToken,
116    ) -> Result<(), BoundedRegularFileReadError> {
117        if cancel.is_cancelled() {
118            return Err(BoundedRegularFileReadError::Cancelled);
119        }
120        Ok(())
121    }
122
123    fn native_name(path: &Path) -> NativeName {
124        NativeName::unix(
125            path.file_name()
126                .unwrap_or(path.as_os_str())
127                .as_bytes()
128                .to_vec(),
129        )
130    }
131
132    fn path_c_string(path: &Path) -> Result<CString, io::Error> {
133        CString::new(path.as_os_str().as_bytes()).map_err(|_| {
134            io::Error::new(io::ErrorKind::InvalidInput, "path contains an interior NUL")
135        })
136    }
137
138    fn child_name_c_string(name: &NativeName) -> Result<CString, io::Error> {
139        let NativeName::UnixBytes(bytes) = name else {
140            return Err(io::Error::new(
141                io::ErrorKind::InvalidInput,
142                "linux child name is not encoded as Unix bytes",
143            ));
144        };
145        if bytes.is_empty()
146            || bytes == b"."
147            || bytes == b".."
148            || bytes.contains(&b'/')
149            || bytes.contains(&0)
150        {
151            return Err(io::Error::new(
152                io::ErrorKind::InvalidInput,
153                "child token is not a single safe basename",
154            ));
155        }
156        CString::new(bytes.as_slice()).map_err(|_| {
157            io::Error::new(
158                io::ErrorKind::InvalidInput,
159                "child name contains an interior NUL",
160            )
161        })
162    }
163
164    fn open_root(path: &Path) -> Result<OwnedFd, io::Error> {
165        let path = Self::path_c_string(path)?;
166        // SAFETY: `open_how` is a plain kernel ABI value initialized to zero,
167        // then populated with documented flags.
168        let mut how: libc::open_how = unsafe { mem::zeroed() };
169        how.flags =
170            u64::try_from(libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
171                .expect("open flags fit u64");
172        how.resolve = libc::RESOLVE_NO_SYMLINKS | libc::RESOLVE_NO_MAGICLINKS;
173        // SAFETY: the path and open_how pointers remain valid for the syscall.
174        let raw = unsafe {
175            libc::syscall(
176                libc::SYS_openat2,
177                libc::AT_FDCWD,
178                path.as_ptr(),
179                &how,
180                mem::size_of::<libc::open_how>(),
181            ) as libc::c_int
182        };
183        Self::owned_fd(raw)
184    }
185
186    fn pin_child(parent: &OwnedFd, name: &CStr) -> Result<OwnedFd, io::Error> {
187        // O_PATH | O_NOFOLLOW pins the directory entry itself, including a
188        // final symlink, without granting read access or following its target.
189        // The token is one validated basename, and RESOLVE_BENEATH keeps the
190        // lookup rooted at the retained parent descriptor.
191        // SAFETY: `open_how` is a plain kernel ABI value initialized to zero,
192        // then populated with documented flags.
193        let mut how: libc::open_how = unsafe { mem::zeroed() };
194        how.flags = u64::try_from(libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC)
195            .expect("open flags fit u64");
196        how.resolve = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_MAGICLINKS;
197        // SAFETY: the basename and open_how pointers remain valid for the
198        // syscall, and `parent` is a live directory descriptor.
199        let raw = unsafe {
200            libc::syscall(
201                libc::SYS_openat2,
202                parent.as_raw_fd(),
203                name.as_ptr(),
204                &how,
205                mem::size_of::<libc::open_how>(),
206            ) as libc::c_int
207        };
208        Self::owned_fd(raw)
209    }
210
211    fn open_child_directory(parent: &OwnedFd, name: &CStr) -> Result<OwnedFd, io::Error> {
212        // SAFETY: `open_how` is a plain kernel ABI value initialized to zero,
213        // then populated with documented flags.
214        let mut how: libc::open_how = unsafe { mem::zeroed() };
215        how.flags =
216            u64::try_from(libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
217                .expect("open flags fit u64");
218        how.resolve = libc::RESOLVE_BENEATH
219            | libc::RESOLVE_NO_SYMLINKS
220            | libc::RESOLVE_NO_MAGICLINKS
221            | libc::RESOLVE_NO_XDEV;
222        // SAFETY: the basename and open_how pointers remain valid for the
223        // syscall, and `parent` is a live directory descriptor.
224        let raw = unsafe {
225            libc::syscall(
226                libc::SYS_openat2,
227                parent.as_raw_fd(),
228                name.as_ptr(),
229                &how,
230                mem::size_of::<libc::open_how>(),
231            ) as libc::c_int
232        };
233        Self::owned_fd(raw)
234    }
235
236    fn open_child_regular_file(parent: &OwnedFd, name: &CStr) -> Result<OwnedFd, io::Error> {
237        // SAFETY: `open_how` is a plain kernel ABI value initialized to zero,
238        // then populated with documented flags.
239        let mut how: libc::open_how = unsafe { mem::zeroed() };
240        how.flags =
241            u64::try_from(libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC)
242                .expect("open flags fit u64");
243        how.resolve = libc::RESOLVE_BENEATH
244            | libc::RESOLVE_NO_SYMLINKS
245            | libc::RESOLVE_NO_MAGICLINKS
246            | libc::RESOLVE_NO_XDEV;
247        // SAFETY: the basename and open_how pointers remain valid for the
248        // syscall, and `parent` is a live directory descriptor.
249        let raw = unsafe {
250            libc::syscall(
251                libc::SYS_openat2,
252                parent.as_raw_fd(),
253                name.as_ptr(),
254                &how,
255                mem::size_of::<libc::open_how>(),
256            ) as libc::c_int
257        };
258        Self::owned_fd(raw)
259    }
260
261    fn open_matching_child_directory(
262        parent: &OwnedFd,
263        name: &CStr,
264        pinned_stat: &libc::stat,
265        pinned_mount_id: u64,
266    ) -> Result<OwnedFd, io::Error> {
267        let directory = Self::open_child_directory(parent, name)?;
268        let directory_stat = Self::fstat(&directory)?;
269        let directory_mount_id = Self::mount_id_for_fd(&directory)?;
270        if !Self::same_object(pinned_stat, &directory_stat) || pinned_mount_id != directory_mount_id
271        {
272            return Err(io::Error::other(
273                "directory entry changed between pinning and enumerable open",
274            ));
275        }
276        Ok(directory)
277    }
278
279    fn owned_fd(raw: libc::c_int) -> Result<OwnedFd, io::Error> {
280        if raw < 0 {
281            return Err(io::Error::last_os_error());
282        }
283        // SAFETY: a successful open-style syscall returns a fresh descriptor.
284        Ok(unsafe { OwnedFd::from_raw_fd(raw) })
285    }
286
287    fn duplicate_fd(fd: &OwnedFd) -> Result<OwnedFd, io::Error> {
288        // SAFETY: fd is live and F_DUPFD_CLOEXEC returns a fresh descriptor.
289        let raw = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) };
290        Self::owned_fd(raw)
291    }
292
293    fn fstat(fd: &OwnedFd) -> Result<libc::stat, io::Error> {
294        let mut output = MaybeUninit::<libc::stat>::uninit();
295        // SAFETY: output points to writable storage and fd remains live.
296        if unsafe { libc::fstat(fd.as_raw_fd(), output.as_mut_ptr()) } != 0 {
297            return Err(io::Error::last_os_error());
298        }
299        // SAFETY: successful fstat initialized the output.
300        Ok(unsafe { output.assume_init() })
301    }
302
303    fn statx_for_fd_with_mask(fd: &OwnedFd, mask: libc::c_uint) -> Result<libc::statx, io::Error> {
304        let mut output = MaybeUninit::<libc::statx>::zeroed();
305        // SAFETY: output is writable, fd is live, and AT_EMPTY_PATH requests
306        // metadata for that already-opened descriptor rather than performing a
307        // second pathname lookup.
308        if unsafe {
309            libc::statx(
310                fd.as_raw_fd(),
311                c"".as_ptr(),
312                libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW,
313                mask,
314                output.as_mut_ptr(),
315            )
316        } != 0
317        {
318            return Err(io::Error::last_os_error());
319        }
320        // SAFETY: successful statx initialized the output.
321        let output = unsafe { output.assume_init() };
322        if output.stx_mask & mask != mask {
323            return Err(io::Error::new(
324                io::ErrorKind::Unsupported,
325                format!(
326                    "statx omitted required regular-file fields: mask=0x{:x}",
327                    output.stx_mask
328                ),
329            ));
330        }
331        Ok(output)
332    }
333
334    fn statx_for_fd(fd: &OwnedFd) -> Result<libc::statx, io::Error> {
335        Self::statx_for_fd_with_mask(fd, libc::STATX_BASIC_STATS | libc::STATX_MNT_ID)
336    }
337
338    fn mount_id_for_fd(fd: &OwnedFd) -> Result<u64, io::Error> {
339        Ok(Self::statx_for_fd_with_mask(fd, libc::STATX_MNT_ID)?.stx_mnt_id)
340    }
341
342    fn kind_from_mode(mode: libc::mode_t) -> EntryKind {
343        match mode & libc::S_IFMT {
344            libc::S_IFDIR => EntryKind::Directory,
345            libc::S_IFREG => EntryKind::File,
346            libc::S_IFLNK => EntryKind::Symlink,
347            _ => EntryKind::Other,
348        }
349    }
350
351    fn metadata_to_entry(
352        path: &Path,
353        file_name: NativeName,
354        stat: &libc::stat,
355        mount_id: u64,
356    ) -> EntryMetadata {
357        let kind = Self::kind_from_mode(stat.st_mode);
358        let logical_bytes = if kind == EntryKind::File {
359            known_u128(stat.st_size.max(0) as u128)
360        } else {
361            known_u128(0)
362        };
363        let allocated_bytes = if kind == EntryKind::File {
364            known_u128(stat.st_blocks.max(0) as u128 * 512)
365        } else {
366            known_u128(0)
367        };
368        let device = stat.st_dev;
369        let inode = stat.st_ino;
370        let identity = EntryIdentity::from_unix(device, inode);
371        let filesystem_identity = Some(FilesystemIdentity { device });
372        let hard_link_key = (kind == EntryKind::File).then(|| HardLinkKey::from(identity.clone()));
373        let fingerprint = fingerprint_for(Some(&identity), &kind, &logical_bytes);
374
375        EntryMetadata {
376            path: path.to_path_buf(),
377            file_name,
378            kind,
379            logical_bytes,
380            allocated_bytes,
381            hard_link_count: known_count(stat.st_nlink as u128),
382            fingerprint,
383            identity: Some(identity),
384            filesystem_identity,
385            mount_identity: Some(MountIdentity { value: mount_id }),
386            hard_link_key,
387        }
388    }
389
390    fn error_entry(path: &Path, error: io::Error) -> WalkEntry<LinuxDirectoryHandle> {
391        WalkEntry::Error(ErrorRecord {
392            path: path.to_path_buf(),
393            kind: error_kind_for_io(&error),
394            reason: reason_for_io(&error),
395            detail: error.to_string(),
396        })
397    }
398
399    fn same_object(left: &libc::stat, right: &libc::stat) -> bool {
400        left.st_dev == right.st_dev
401            && left.st_ino == right.st_ino
402            && Self::kind_from_mode(left.st_mode) == Self::kind_from_mode(right.st_mode)
403    }
404
405    fn regular_file_change_stamp(statx: &libc::statx) -> RegularFileChangeStamp {
406        let mut bytes = Vec::with_capacity(
407            mem::size_of_val(&statx.stx_mtime.tv_sec)
408                + mem::size_of_val(&statx.stx_mtime.tv_nsec)
409                + mem::size_of_val(&statx.stx_ctime.tv_sec)
410                + mem::size_of_val(&statx.stx_ctime.tv_nsec),
411        );
412        bytes.extend_from_slice(&statx.stx_mtime.tv_sec.to_le_bytes());
413        bytes.extend_from_slice(&statx.stx_mtime.tv_nsec.to_le_bytes());
414        bytes.extend_from_slice(&statx.stx_ctime.tv_sec.to_le_bytes());
415        bytes.extend_from_slice(&statx.stx_ctime.tv_nsec.to_le_bytes());
416        RegularFileChangeStamp::new(bytes)
417    }
418
419    fn observe_regular_file_fd(fd: &OwnedFd) -> Result<RegularFileObservation, io::Error> {
420        let statx = Self::statx_for_fd(fd)?;
421        let device = libc::makedev(statx.stx_dev_major, statx.stx_dev_minor) as u64;
422        Ok(RegularFileObservation {
423            kind: Self::kind_from_mode(statx.stx_mode.into()),
424            identity: EntryIdentity::from_unix(device, statx.stx_ino),
425            filesystem_identity: FilesystemIdentity { device },
426            mount_identity: MountIdentity {
427                value: statx.stx_mnt_id,
428            },
429            logical_bytes: DecimalU128::new(u128::from(statx.stx_size)),
430            change_stamp: Self::regular_file_change_stamp(&statx),
431        })
432    }
433
434    fn classify_pinned_regular_file(
435        pinned: &OwnedFd,
436    ) -> Result<RegularFileObservation, BoundedRegularFileReadError> {
437        let observed = Self::observe_regular_file_fd(pinned).map_err(|error| {
438            if error.kind() == io::ErrorKind::Unsupported {
439                BoundedRegularFileReadError::Unsupported(error.to_string())
440            } else {
441                BoundedRegularFileReadError::io(error)
442            }
443        })?;
444        match observed.kind {
445            EntryKind::File => Ok(observed),
446            EntryKind::Symlink | EntryKind::ReparsePoint => {
447                Err(BoundedRegularFileReadError::SymlinkOrReparse {
448                    observed_kind: observed.kind,
449                })
450            }
451            _ => Err(BoundedRegularFileReadError::NotRegular {
452                observed_kind: observed.kind,
453            }),
454        }
455    }
456
457    fn map_regular_file_open_error(error: io::Error) -> BoundedRegularFileReadError {
458        match error.raw_os_error() {
459            Some(libc::ENOENT) => BoundedRegularFileReadError::NotFound,
460            Some(libc::ELOOP) => BoundedRegularFileReadError::SymlinkOrReparse {
461                observed_kind: EntryKind::Symlink,
462            },
463            Some(libc::ENOSYS) => BoundedRegularFileReadError::Unsupported(
464                "openat2 is required for safe Linux bounded regular-file reads".to_string(),
465            ),
466            _ => BoundedRegularFileReadError::io(error),
467        }
468    }
469
470    fn map_regular_file_read_error(error: io::Error) -> BoundedRegularFileReadError {
471        match error.raw_os_error() {
472            Some(libc::ENODEV) | Some(libc::ENXIO) => {
473                BoundedRegularFileReadError::ProviderOrOffline(error.to_string())
474            }
475            Some(libc::EOPNOTSUPP) => BoundedRegularFileReadError::Unsupported(error.to_string()),
476            _ => BoundedRegularFileReadError::io(error),
477        }
478    }
479
480    fn validate_regular_file_unchanged(
481        observed_before: &RegularFileObservation,
482        observed_after: &RegularFileObservation,
483    ) -> Result<(), BoundedRegularFileReadError> {
484        if observed_before.mount_identity != observed_after.mount_identity {
485            return Err(BoundedRegularFileReadError::MountMismatch(Box::new(
486                RegularFileMountMismatch {
487                    expected: None,
488                    observed_mount_identity: observed_after.mount_identity.clone(),
489                },
490            )));
491        }
492        if observed_before.identity != observed_after.identity
493            || observed_before.filesystem_identity != observed_after.filesystem_identity
494        {
495            return Err(BoundedRegularFileReadError::IdentityMismatch(Box::new(
496                RegularFileIdentityMismatch {
497                    expected: None,
498                    observed_identity: observed_after.identity.clone(),
499                    observed_filesystem_identity: observed_after.filesystem_identity.clone(),
500                },
501            )));
502        }
503        if observed_before.logical_bytes != observed_after.logical_bytes
504            || observed_before.change_stamp != observed_after.change_stamp
505        {
506            return Err(BoundedRegularFileReadError::ChangedDuringRead(Box::new(
507                sweepx_platform::RegularFileObservationMismatch {
508                    observed_before: observed_before.clone(),
509                    observed_after: observed_after.clone(),
510                },
511            )));
512        }
513        Ok(())
514    }
515
516    fn compare_expected_regular_file(
517        request: &BoundedRegularFileReadRequest,
518        observed_before: &RegularFileObservation,
519    ) -> Result<(), BoundedRegularFileReadError> {
520        let RegularFileReadExpectation::PreviouslyObserved(expected) = request.expectation() else {
521            return Ok(());
522        };
523        if expected.mount_identity() != &observed_before.mount_identity {
524            return Err(BoundedRegularFileReadError::MountMismatch(Box::new(
525                RegularFileMountMismatch {
526                    expected: Some(expected.clone()),
527                    observed_mount_identity: observed_before.mount_identity.clone(),
528                },
529            )));
530        }
531        if expected.identity() != &observed_before.identity
532            || expected.filesystem_identity() != &observed_before.filesystem_identity
533        {
534            return Err(BoundedRegularFileReadError::IdentityMismatch(Box::new(
535                RegularFileIdentityMismatch {
536                    expected: Some(expected.clone()),
537                    observed_identity: observed_before.identity.clone(),
538                    observed_filesystem_identity: observed_before.filesystem_identity.clone(),
539                },
540            )));
541        }
542        Ok(())
543    }
544
545    fn compare_pinned_and_opened_regular_file(
546        pinned: &RegularFileObservation,
547        observed_before: &RegularFileObservation,
548    ) -> Result<(), BoundedRegularFileReadError> {
549        if pinned.mount_identity != observed_before.mount_identity {
550            return Err(BoundedRegularFileReadError::MountMismatch(Box::new(
551                RegularFileMountMismatch {
552                    expected: None,
553                    observed_mount_identity: observed_before.mount_identity.clone(),
554                },
555            )));
556        }
557        if pinned.identity != observed_before.identity
558            || pinned.filesystem_identity != observed_before.filesystem_identity
559        {
560            return Err(BoundedRegularFileReadError::IdentityMismatch(Box::new(
561                RegularFileIdentityMismatch {
562                    expected: None,
563                    observed_identity: observed_before.identity.clone(),
564                    observed_filesystem_identity: observed_before.filesystem_identity.clone(),
565                },
566            )));
567        }
568        Ok(())
569    }
570
571    fn observed_limit_exceeded(
572        request: &BoundedRegularFileReadRequest,
573        observed_logical_bytes: DecimalU128,
574    ) -> BoundedRegularFileReadError {
575        BoundedRegularFileReadError::LimitExceeded {
576            max_bytes: request.max_bytes(),
577            observed_logical_bytes,
578        }
579    }
580
581    #[cfg(all(test, target_os = "linux"))]
582    fn run_regular_file_read_test_hook(
583        event: RegularFileReadTestHookEvent,
584        cancel: &CancellationToken,
585    ) {
586        let hook = REGULAR_FILE_READ_TEST_HOOK.with(|slot| {
587            let mut slot = slot.borrow_mut();
588            match slot.as_ref() {
589                Some(installed) if installed.event == event => slot.take(),
590                _ => None,
591            }
592        });
593        match hook.map(|hook| hook.action) {
594            Some(RegularFileReadTestHookAction::Cancel) => cancel.cancel(),
595            Some(RegularFileReadTestHookAction::RewriteBytes { path, bytes }) => {
596                std::fs::write(path, bytes).unwrap();
597            }
598            None => {}
599        }
600    }
601}
602
603#[cfg(target_os = "linux")]
604impl PlatformScanner for LinuxPlatformScanner {
605    type DirectoryHandle = LinuxDirectoryHandle;
606
607    fn platform_name(&self) -> &'static str {
608        "linux"
609    }
610
611    fn admit_root(
612        &self,
613        root: &ScanRoot,
614        cancel: &CancellationToken,
615    ) -> Result<RootAdmission<Self::DirectoryHandle>, PlatformError> {
616        Self::ensure_not_cancelled(cancel)?;
617        if !root.path().is_absolute() {
618            return Err(PlatformError::RootRejected(format!(
619                "root is not absolute: {}",
620                root.path().display()
621            )));
622        }
623        let root_locator = root
624            .native_absolute_path()
625            .map_err(|error| PlatformError::RootRejected(error.to_string()))?;
626
627        let fd = Self::open_root(root.path()).map_err(|error| {
628            if matches!(
629                error.raw_os_error(),
630                Some(libc::ELOOP) | Some(libc::ENOTDIR)
631            ) {
632                PlatformError::RootRejected(format!(
633                    "root or an intermediate component is not a real directory: {}",
634                    root.path().display()
635                ))
636            } else if error.raw_os_error() == Some(libc::ENOSYS) {
637                PlatformError::Unsupported(
638                    "openat2 is required for safe Linux root admission".to_string(),
639                )
640            } else {
641                PlatformError::io(root.path(), error)
642            }
643        })?;
644        Self::ensure_not_cancelled(cancel)?;
645        let stat = Self::fstat(&fd).map_err(|error| PlatformError::io(root.path(), error))?;
646        if Self::kind_from_mode(stat.st_mode) != EntryKind::Directory {
647            return Err(PlatformError::RootRejected(format!(
648                "root is not a directory: {}",
649                root.path().display()
650            )));
651        }
652        let mount_id = Self::mount_id_for_fd(&fd).map_err(|error| {
653            PlatformError::Unsupported(format!(
654                "statx mount identity unavailable for {}: {error}",
655                root.path().display()
656            ))
657        })?;
658        let metadata =
659            Self::metadata_to_entry(root.path(), Self::native_name(root.path()), &stat, mount_id);
660        Ok(RootAdmission::new(
661            root.clone(),
662            metadata,
663            LinuxDirectoryHandle {
664                fd,
665                display_path: root.path().to_path_buf(),
666                cursor: DirectoryCursor::NotStarted,
667            },
668            root_locator,
669        ))
670    }
671
672    fn enumerate_children(
673        &self,
674        directory: &mut Self::DirectoryHandle,
675        cancel: &CancellationToken,
676        limits: DirectoryReadLimits,
677    ) -> Result<DirectoryEntryBatch, PlatformError> {
678        Self::ensure_not_cancelled(cancel)?;
679        if limits.max_batch_entries == 0 || limits.max_batch_bytes == 0 {
680            return Err(PlatformError::ResourceLimit(format!(
681                "directory batch limits must be nonzero at {}",
682                directory.display_path.display()
683            )));
684        }
685        if matches!(directory.cursor, DirectoryCursor::NotStarted) {
686            let duplicate = Self::duplicate_fd(&directory.fd)
687                .map_err(|error| PlatformError::io(&directory.display_path, error))?;
688            let raw = duplicate.as_raw_fd();
689            // SAFETY: raw is a live directory descriptor. On success fdopendir
690            // takes ownership, so the OwnedFd is forgotten immediately afterward.
691            let stream = unsafe { libc::fdopendir(raw) };
692            if stream.is_null() {
693                return Err(PlatformError::io(
694                    &directory.display_path,
695                    io::Error::last_os_error(),
696                ));
697            }
698            mem::forget(duplicate);
699            directory.cursor = DirectoryCursor::Active {
700                stream: DirectoryStream(stream),
701                pending: None,
702            };
703        }
704        let DirectoryCursor::Active { stream, pending } = &mut directory.cursor else {
705            unreachable!("nonterminal directory cursor must be active");
706        };
707        let mut entries = Vec::new();
708        let mut retained_bytes = 0usize;
709
710        loop {
711            Self::ensure_not_cancelled(cancel)?;
712            let child = if let Some(child) = pending.take() {
713                child
714            } else {
715                loop {
716                    // POSIX requires errno to be cleared to distinguish EOF from error.
717                    // SAFETY: __errno_location returns the calling thread's errno slot.
718                    unsafe {
719                        *libc::__errno_location() = 0;
720                    }
721                    // SAFETY: stream is live and exclusively used by this loop.
722                    let raw_entry = unsafe { libc::readdir(stream.0) };
723                    if raw_entry.is_null() {
724                        let error = io::Error::last_os_error();
725                        if error.raw_os_error() == Some(0) {
726                            return Ok(DirectoryEntryBatch::complete(entries));
727                        }
728                        return Err(PlatformError::io(&directory.display_path, error));
729                    }
730                    // SAFETY: readdir returned a valid dirent whose d_name is NUL terminated.
731                    let name = unsafe { CStr::from_ptr((*raw_entry).d_name.as_ptr()) }.to_bytes();
732                    if name == b"." || name == b".." {
733                        continue;
734                    }
735                    break DirectoryEntryRecord::from_parent_and_name(
736                        &directory.display_path,
737                        NativeName::unix(name.to_vec()),
738                    )
739                    .map_err(|error| PlatformError::InvalidDirectoryEntry {
740                        parent: directory.display_path.clone(),
741                        detail: error.to_string(),
742                    })?;
743                }
744            };
745            let record_bytes = child.estimated_retained_bytes().ok_or_else(|| {
746                PlatformError::ResourceLimit(format!(
747                    "directory byte accounting overflow at {}",
748                    directory.display_path.display()
749                ))
750            })?;
751            let next_bytes = retained_bytes.checked_add(record_bytes).ok_or_else(|| {
752                PlatformError::ResourceLimit(format!(
753                    "directory byte accounting overflow at {}",
754                    directory.display_path.display()
755                ))
756            })?;
757            if entries.is_empty() && record_bytes > limits.max_batch_bytes {
758                *pending = Some(child);
759                return Err(PlatformError::ResourceLimit(format!(
760                    "single directory entry exceeds the retained-byte cap at {}",
761                    directory.display_path.display()
762                )));
763            }
764            if entries.len() >= limits.max_batch_entries || next_bytes > limits.max_batch_bytes {
765                *pending = Some(child);
766                return Ok(DirectoryEntryBatch::continued(entries));
767            }
768            retained_bytes = next_bytes;
769            entries.push(child);
770        }
771    }
772
773    fn inspect_child(
774        &self,
775        parent: &Self::DirectoryHandle,
776        child: &DirectoryEntryRecord,
777        cancel: &CancellationToken,
778    ) -> Result<WalkEntry<Self::DirectoryHandle>, PlatformError> {
779        self.inspect_child_with_directory_admission(
780            parent,
781            child,
782            cancel,
783            DirectoryHandleAdmission::Allow,
784        )
785    }
786
787    fn inspect_child_with_directory_admission(
788        &self,
789        parent: &Self::DirectoryHandle,
790        child: &DirectoryEntryRecord,
791        cancel: &CancellationToken,
792        directory_admission: DirectoryHandleAdmission,
793    ) -> Result<WalkEntry<Self::DirectoryHandle>, PlatformError> {
794        child
795            .validate_for_parent(&parent.display_path)
796            .map_err(|error| PlatformError::InvalidDirectoryEntry {
797                parent: parent.display_path.clone(),
798                detail: error.to_string(),
799            })?;
800        Self::ensure_not_cancelled(cancel)?;
801        let name = match Self::child_name_c_string(&child.file_name) {
802            Ok(name) => name,
803            Err(error) => return Ok(Self::error_entry(&child.path, error)),
804        };
805        let path = child.path.clone();
806        let pinned = match Self::pin_child(&parent.fd, &name) {
807            Ok(fd) => fd,
808            Err(error) => return Ok(Self::error_entry(&path, error)),
809        };
810        Self::ensure_not_cancelled(cancel)?;
811        let pinned_stat = match Self::fstat(&pinned) {
812            Ok(stat) => stat,
813            Err(error) => return Ok(Self::error_entry(&path, error)),
814        };
815        let pinned_mount_id = match Self::mount_id_for_fd(&pinned) {
816            Ok(value) => value,
817            Err(error) => return Ok(Self::error_entry(&path, error)),
818        };
819
820        if Self::kind_from_mode(pinned_stat.st_mode) == EntryKind::Directory {
821            if directory_admission == DirectoryHandleAdmission::Deny {
822                return Ok(WalkEntry::Boundary(BoundaryRecord {
823                    path,
824                    kind: BoundaryKind::ResourceLimit,
825                    reason: ReasonCode::ResourceLimit,
826                    detail: "frontier limit exceeded".to_string(),
827                }));
828            }
829            let directory_fd = match Self::open_matching_child_directory(
830                &parent.fd,
831                &name,
832                &pinned_stat,
833                pinned_mount_id,
834            ) {
835                Ok(fd) => fd,
836                Err(error) if error.raw_os_error() == Some(libc::EXDEV) => {
837                    return Ok(WalkEntry::Boundary(BoundaryRecord {
838                        path,
839                        kind: BoundaryKind::Mount,
840                        reason: ReasonCode::UnsupportedFilesystem,
841                        detail: format!(
842                            "entry crosses a mount boundary (mount id {})",
843                            pinned_mount_id
844                        ),
845                    }));
846                }
847                Err(error) => return Ok(Self::error_entry(&path, error)),
848            };
849            let metadata = Self::metadata_to_entry(
850                &path,
851                child.file_name.clone(),
852                &pinned_stat,
853                pinned_mount_id,
854            );
855            return Ok(WalkEntry::Directory(OpenedDirectory {
856                metadata,
857                handle: LinuxDirectoryHandle {
858                    fd: directory_fd,
859                    display_path: path,
860                    cursor: DirectoryCursor::NotStarted,
861                },
862            }));
863        }
864
865        let metadata = Self::metadata_to_entry(
866            &path,
867            child.file_name.clone(),
868            &pinned_stat,
869            pinned_mount_id,
870        );
871        match metadata.kind {
872            EntryKind::Directory => unreachable!("directories returned above"),
873            EntryKind::File => Ok(WalkEntry::File(metadata)),
874            EntryKind::Symlink => Ok(WalkEntry::Link(metadata)),
875            EntryKind::ReparsePoint => Ok(WalkEntry::Boundary(BoundaryRecord {
876                path,
877                kind: BoundaryKind::ReparsePoint,
878                reason: ReasonCode::UnsupportedFilesystem,
879                detail: "directory reparse points are unsupported on linux backend".to_string(),
880            })),
881            EntryKind::Other => Ok(WalkEntry::Boundary(BoundaryRecord {
882                path,
883                kind: BoundaryKind::OtherFilesystem,
884                reason: ReasonCode::UnsupportedFilesystem,
885                detail: "special filesystem entry rejected".to_string(),
886            })),
887        }
888    }
889
890    fn is_same_mount(
891        &self,
892        root: &EntryMetadata,
893        entry: &EntryMetadata,
894    ) -> Result<bool, PlatformError> {
895        match (&root.mount_identity, &entry.mount_identity) {
896            (Some(left), Some(right)) => Ok(left == right),
897            _ => Err(PlatformError::Unsupported(
898                "mount identity unavailable".to_string(),
899            )),
900        }
901    }
902
903    fn read_regular_file_relative(
904        &self,
905        parent: &Self::DirectoryHandle,
906        request: &BoundedRegularFileReadRequest,
907        cancel: &CancellationToken,
908    ) -> Result<PresentRegularFileRead, BoundedRegularFileReadError> {
909        Self::ensure_regular_file_read_not_cancelled(cancel)?;
910        let name = Self::child_name_c_string(request.child_name()).map_err(|error| {
911            if error.kind() == io::ErrorKind::InvalidInput {
912                BoundedRegularFileReadError::UnsafeName
913            } else {
914                BoundedRegularFileReadError::io(error)
915            }
916        })?;
917        let pinned =
918            Self::pin_child(&parent.fd, &name).map_err(Self::map_regular_file_open_error)?;
919        Self::ensure_regular_file_read_not_cancelled(cancel)?;
920        let pinned_observation = Self::classify_pinned_regular_file(&pinned)?;
921        let read_fd = Self::open_child_regular_file(&parent.fd, &name)
922            .map_err(Self::map_regular_file_open_error)?;
923        let observed_before = Self::observe_regular_file_fd(&read_fd).map_err(|error| {
924            if error.kind() == io::ErrorKind::Unsupported {
925                BoundedRegularFileReadError::Unsupported(error.to_string())
926            } else {
927                BoundedRegularFileReadError::io(error)
928            }
929        })?;
930        Self::compare_pinned_and_opened_regular_file(&pinned_observation, &observed_before)?;
931        Self::compare_expected_regular_file(request, &observed_before)?;
932        Self::ensure_regular_file_read_not_cancelled(cancel)?;
933
934        let mut bytes = Vec::new();
935        let hard_cap = request.max_bytes().saturating_add(1);
936        let mut buffer = [0u8; Self::REGULAR_FILE_READ_CHUNK_BYTES];
937        let mut overflow_lower_bound = (observed_before.logical_bytes
938            > DecimalU128::new(request.max_bytes() as u128))
939        .then_some(observed_before.logical_bytes);
940        loop {
941            if overflow_lower_bound.is_some() {
942                break;
943            }
944            Self::ensure_regular_file_read_not_cancelled(cancel)?;
945            let remaining = hard_cap.saturating_sub(bytes.len());
946            if remaining == 0 {
947                overflow_lower_bound = Some(DecimalU128::new(
948                    (request.max_bytes() as u128).saturating_add(1),
949                ));
950                break;
951            }
952            let chunk_len = remaining.min(buffer.len());
953            // SAFETY: the fd is live, the buffer is writable for `chunk_len`
954            // bytes, and the kernel writes at most that much.
955            let read =
956                unsafe { libc::read(read_fd.as_raw_fd(), buffer.as_mut_ptr().cast(), chunk_len) };
957            if read < 0 {
958                let error = io::Error::last_os_error();
959                if error.raw_os_error() == Some(libc::EINTR) {
960                    Self::ensure_regular_file_read_not_cancelled(cancel)?;
961                    continue;
962                }
963                return Err(Self::map_regular_file_read_error(error));
964            }
965            let read = read as usize;
966            if read == 0 {
967                break;
968            }
969            bytes.extend_from_slice(&buffer[..read]);
970            #[cfg(test)]
971            Self::run_regular_file_read_test_hook(
972                RegularFileReadTestHookEvent::AfterReadChunk,
973                cancel,
974            );
975            if bytes.len() > request.max_bytes() {
976                overflow_lower_bound = Some(
977                    overflow_lower_bound
978                        .unwrap_or(DecimalU128::ZERO)
979                        .max(DecimalU128::new(bytes.len() as u128)),
980                );
981                break;
982            }
983        }
984
985        #[cfg(test)]
986        Self::run_regular_file_read_test_hook(
987            RegularFileReadTestHookEvent::BeforeObservedAfter,
988            cancel,
989        );
990        let observed_after = Self::observe_regular_file_fd(&read_fd).map_err(|error| {
991            if error.kind() == io::ErrorKind::Unsupported {
992                BoundedRegularFileReadError::Unsupported(error.to_string())
993            } else {
994                BoundedRegularFileReadError::io(error)
995            }
996        })?;
997        #[cfg(test)]
998        Self::run_regular_file_read_test_hook(
999            RegularFileReadTestHookEvent::AfterObservedAfter,
1000            cancel,
1001        );
1002        Self::ensure_regular_file_read_not_cancelled(cancel)?;
1003        Self::validate_regular_file_unchanged(&observed_before, &observed_after)?;
1004        if let Some(lower_bound) = overflow_lower_bound {
1005            let observed_logical_bytes = if observed_after.logical_bytes > lower_bound {
1006                observed_after.logical_bytes
1007            } else {
1008                lower_bound
1009            };
1010            return Err(Self::observed_limit_exceeded(
1011                request,
1012                observed_logical_bytes,
1013            ));
1014        }
1015        Ok(PresentRegularFileRead {
1016            bytes,
1017            observed_before,
1018            observed_after,
1019        })
1020    }
1021}
1022
1023/// Reports the portable fail-closed contract on hosts without the Linux backend.
1024///
1025/// A missing backend must never degrade into pathname-based traversal, so every
1026/// operation reports `Unsupported` rather than attempting a weaker scan.
1027#[cfg(not(target_os = "linux"))]
1028impl PlatformScanner for LinuxPlatformScanner {
1029    type DirectoryHandle = LinuxUnavailableDirectory;
1030
1031    fn platform_name(&self) -> &'static str {
1032        "linux"
1033    }
1034
1035    fn admit_root(
1036        &self,
1037        _root: &ScanRoot,
1038        _cancel: &CancellationToken,
1039    ) -> Result<RootAdmission<Self::DirectoryHandle>, PlatformError> {
1040        Err(PlatformError::Unsupported(
1041            "Linux scanner backend is unavailable on this host".to_string(),
1042        ))
1043    }
1044
1045    fn enumerate_children(
1046        &self,
1047        _directory: &mut Self::DirectoryHandle,
1048        _cancel: &CancellationToken,
1049        _limits: DirectoryReadLimits,
1050    ) -> Result<DirectoryEntryBatch, PlatformError> {
1051        Err(PlatformError::Unsupported(
1052            "Linux scanner backend is unavailable on this host".to_string(),
1053        ))
1054    }
1055
1056    fn inspect_child(
1057        &self,
1058        _parent: &Self::DirectoryHandle,
1059        _child: &DirectoryEntryRecord,
1060        _cancel: &CancellationToken,
1061    ) -> Result<WalkEntry<Self::DirectoryHandle>, PlatformError> {
1062        Err(PlatformError::Unsupported(
1063            "Linux scanner backend is unavailable on this host".to_string(),
1064        ))
1065    }
1066
1067    fn inspect_child_with_directory_admission(
1068        &self,
1069        parent: &Self::DirectoryHandle,
1070        child: &DirectoryEntryRecord,
1071        cancel: &CancellationToken,
1072        _directory_admission: DirectoryHandleAdmission,
1073    ) -> Result<WalkEntry<Self::DirectoryHandle>, PlatformError> {
1074        self.inspect_child(parent, child, cancel)
1075    }
1076
1077    fn is_same_mount(
1078        &self,
1079        _root: &EntryMetadata,
1080        _entry: &EntryMetadata,
1081    ) -> Result<bool, PlatformError> {
1082        Err(PlatformError::Unsupported(
1083            "Linux scanner backend is unavailable on this host".to_string(),
1084        ))
1085    }
1086}
1087
1088#[cfg(all(test, target_os = "linux"))]
1089#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1090enum RegularFileReadTestHookEvent {
1091    AfterReadChunk,
1092    BeforeObservedAfter,
1093    AfterObservedAfter,
1094}
1095
1096#[cfg(all(test, target_os = "linux"))]
1097#[derive(Debug)]
1098enum RegularFileReadTestHookAction {
1099    Cancel,
1100    RewriteBytes { path: PathBuf, bytes: Vec<u8> },
1101}
1102
1103#[cfg(all(test, target_os = "linux"))]
1104#[derive(Debug)]
1105struct RegularFileReadTestHook {
1106    event: RegularFileReadTestHookEvent,
1107    action: RegularFileReadTestHookAction,
1108}
1109
1110#[cfg(all(test, target_os = "linux"))]
1111thread_local! {
1112    static REGULAR_FILE_READ_TEST_HOOK: RefCell<Option<RegularFileReadTestHook>> = const { RefCell::new(None) };
1113}
1114
1115#[cfg(all(test, target_os = "linux"))]
1116mod tests {
1117    use std::ffi::OsStr;
1118    use std::fs;
1119    use std::os::unix::ffi::OsStrExt;
1120    use std::os::unix::fs::FileTypeExt;
1121    use std::os::unix::fs::MetadataExt;
1122
1123    use super::*;
1124    use sweepx_platform::{BoundedRegularFileReadError, read_bound_regular_file};
1125    use tempfile::TempDir;
1126
1127    fn limits(max_entries: usize) -> DirectoryReadLimits {
1128        DirectoryReadLimits {
1129            max_batch_entries: max_entries,
1130            max_batch_bytes: 1024 * 1024,
1131        }
1132    }
1133
1134    fn find_child(
1135        scanner: &LinuxPlatformScanner,
1136        admission: &mut RootAdmission<LinuxDirectoryHandle>,
1137        name: &[u8],
1138    ) -> DirectoryEntryRecord {
1139        scanner
1140            .enumerate_children(
1141                &mut admission.directory,
1142                &CancellationToken::new(),
1143                limits(64),
1144            )
1145            .unwrap()
1146            .entries
1147            .into_iter()
1148            .find(|entry| entry.file_name == NativeName::unix(name.to_vec()))
1149            .unwrap()
1150    }
1151
1152    fn install_regular_file_read_test_hook(hook: RegularFileReadTestHook) -> impl Drop {
1153        REGULAR_FILE_READ_TEST_HOOK.with(|slot| {
1154            let mut slot = slot.borrow_mut();
1155            assert!(
1156                slot.is_none(),
1157                "regular-file read test hook already installed"
1158            );
1159            *slot = Some(hook);
1160        });
1161        struct HookReset;
1162        impl Drop for HookReset {
1163            fn drop(&mut self) {
1164                REGULAR_FILE_READ_TEST_HOOK.with(|slot| {
1165                    *slot.borrow_mut() = None;
1166                });
1167            }
1168        }
1169        HookReset
1170    }
1171
1172    fn live_read_request(name: &[u8], max_bytes: usize) -> BoundedRegularFileReadRequest {
1173        BoundedRegularFileReadRequest::establish_live(NativeName::unix(name.to_vec()), max_bytes)
1174            .unwrap()
1175    }
1176
1177    #[test]
1178    fn reject_root_symlink() {
1179        let temp = TempDir::new().unwrap();
1180        let target = temp.path().join("target");
1181        fs::create_dir(&target).unwrap();
1182        let link = temp.path().join("link");
1183        std::os::unix::fs::symlink(&target, &link).unwrap();
1184
1185        let scanner = LinuxPlatformScanner::new();
1186        let root = ScanRoot::new(link).unwrap();
1187        let error = scanner
1188            .admit_root(&root, &CancellationToken::new())
1189            .unwrap_err();
1190        assert!(matches!(error, PlatformError::RootRejected(_)));
1191    }
1192
1193    #[test]
1194    fn symlink_child_is_reported_without_following() {
1195        let temp = TempDir::new().unwrap();
1196        fs::write(temp.path().join("target"), b"payload").unwrap();
1197        std::os::unix::fs::symlink("target", temp.path().join("link")).unwrap();
1198
1199        let scanner = LinuxPlatformScanner::new();
1200        let mut admission = scanner
1201            .admit_root(
1202                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1203                &CancellationToken::new(),
1204            )
1205            .unwrap();
1206        let child = find_child(&scanner, &mut admission, b"link");
1207        let entry = scanner
1208            .inspect_child(&admission.directory, &child, &CancellationToken::new())
1209            .unwrap();
1210
1211        assert!(matches!(entry, WalkEntry::Link(_)));
1212    }
1213
1214    #[test]
1215    fn denied_directory_admission_classifies_without_opening_a_retained_handle() {
1216        let temp = TempDir::new().unwrap();
1217        fs::create_dir(temp.path().join("directory")).unwrap();
1218        fs::write(temp.path().join("file"), b"payload").unwrap();
1219        std::os::unix::fs::symlink("file", temp.path().join("link")).unwrap();
1220
1221        let scanner = LinuxPlatformScanner::new();
1222        let mut admission = scanner
1223            .admit_root(
1224                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1225                &CancellationToken::new(),
1226            )
1227            .unwrap();
1228        let children = scanner
1229            .enumerate_children(
1230                &mut admission.directory,
1231                &CancellationToken::new(),
1232                limits(64),
1233            )
1234            .unwrap()
1235            .entries;
1236
1237        for child in children {
1238            let name = child.file_name.clone();
1239            let entry = scanner
1240                .inspect_child_with_directory_admission(
1241                    &admission.directory,
1242                    &child,
1243                    &CancellationToken::new(),
1244                    DirectoryHandleAdmission::Deny,
1245                )
1246                .unwrap();
1247            if name == NativeName::unix(b"directory".to_vec()) {
1248                assert!(matches!(
1249                    entry,
1250                    WalkEntry::Boundary(BoundaryRecord {
1251                        kind: BoundaryKind::ResourceLimit,
1252                        ..
1253                    })
1254                ));
1255            } else if name == NativeName::unix(b"file".to_vec()) {
1256                assert!(matches!(entry, WalkEntry::File(_)));
1257            } else if name == NativeName::unix(b"link".to_vec()) {
1258                assert!(matches!(entry, WalkEntry::Link(_)));
1259            }
1260        }
1261    }
1262
1263    #[test]
1264    fn direct_child_inspection_rejects_forged_parent_binding() {
1265        let temp = TempDir::new().unwrap();
1266        fs::write(temp.path().join("safe"), b"safe").unwrap();
1267        let scanner = LinuxPlatformScanner::new();
1268        let admission = scanner
1269            .admit_root(
1270                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1271                &CancellationToken::new(),
1272            )
1273            .unwrap();
1274        let forged = DirectoryEntryRecord {
1275            path: temp.path().join("different"),
1276            file_name: NativeName::unix(b"safe".to_vec()),
1277        };
1278
1279        let error = scanner
1280            .inspect_child(&admission.directory, &forged, &CancellationToken::new())
1281            .unwrap_err();
1282
1283        assert!(matches!(error, PlatformError::InvalidDirectoryEntry { .. }));
1284    }
1285
1286    #[test]
1287    fn pinned_file_identity_and_mount_survive_name_replacement() {
1288        let temp = TempDir::new().unwrap();
1289        let child_path = temp.path().join("child");
1290        let displaced_path = temp.path().join("displaced");
1291        fs::write(&child_path, b"original").unwrap();
1292
1293        let scanner = LinuxPlatformScanner::new();
1294        let admission = scanner
1295            .admit_root(
1296                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1297                &CancellationToken::new(),
1298            )
1299            .unwrap();
1300        let name = CString::new("child").unwrap();
1301        let pinned = LinuxPlatformScanner::pin_child(&admission.directory.fd, &name).unwrap();
1302        let before = LinuxPlatformScanner::fstat(&pinned).unwrap();
1303        let before_mount = LinuxPlatformScanner::mount_id_for_fd(&pinned).unwrap();
1304
1305        fs::rename(&child_path, &displaced_path).unwrap();
1306        fs::write(&child_path, b"replacement-is-different").unwrap();
1307
1308        let after = LinuxPlatformScanner::fstat(&pinned).unwrap();
1309        let after_mount = LinuxPlatformScanner::mount_id_for_fd(&pinned).unwrap();
1310        let replacement = fs::metadata(&child_path).unwrap();
1311
1312        assert!(LinuxPlatformScanner::same_object(&before, &after));
1313        assert_eq!(before_mount, after_mount);
1314        assert_eq!(after.st_size, 8);
1315        assert_ne!(after.st_ino, replacement.ino());
1316    }
1317
1318    #[test]
1319    fn directory_reopen_rejects_replacement_after_pin() {
1320        let temp = TempDir::new().unwrap();
1321        let child_path = temp.path().join("child");
1322        let displaced_path = temp.path().join("displaced");
1323        fs::create_dir(&child_path).unwrap();
1324
1325        let scanner = LinuxPlatformScanner::new();
1326        let admission = scanner
1327            .admit_root(
1328                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1329                &CancellationToken::new(),
1330            )
1331            .unwrap();
1332        let name = CString::new("child").unwrap();
1333        let pinned = LinuxPlatformScanner::pin_child(&admission.directory.fd, &name).unwrap();
1334        let pinned_stat = LinuxPlatformScanner::fstat(&pinned).unwrap();
1335        let pinned_mount = LinuxPlatformScanner::mount_id_for_fd(&pinned).unwrap();
1336
1337        fs::rename(&child_path, &displaced_path).unwrap();
1338        fs::create_dir(&child_path).unwrap();
1339
1340        let error = LinuxPlatformScanner::open_matching_child_directory(
1341            &admission.directory.fd,
1342            &name,
1343            &pinned_stat,
1344            pinned_mount,
1345        )
1346        .unwrap_err();
1347
1348        assert!(error.to_string().contains("changed between pinning"));
1349    }
1350
1351    #[test]
1352    fn nested_mount_is_a_boundary_before_enumerable_handle_admission() {
1353        if !Path::new("/proc").is_dir() {
1354            return;
1355        }
1356        let scanner = LinuxPlatformScanner::new();
1357        let admission = scanner
1358            .admit_root(
1359                &ScanRoot::new(PathBuf::from("/")).unwrap(),
1360                &CancellationToken::new(),
1361            )
1362            .unwrap();
1363        let name = CString::new("proc").unwrap();
1364        let pinned = LinuxPlatformScanner::pin_child(&admission.directory.fd, &name).unwrap();
1365        let root_mount = admission.metadata.mount_identity.as_ref().unwrap().value;
1366        let proc_mount = LinuxPlatformScanner::mount_id_for_fd(&pinned).unwrap();
1367        if root_mount == proc_mount {
1368            return;
1369        }
1370        let child = DirectoryEntryRecord::from_parent_and_name(
1371            Path::new("/"),
1372            NativeName::unix(b"proc".to_vec()),
1373        )
1374        .unwrap();
1375
1376        let entry = scanner
1377            .inspect_child(&admission.directory, &child, &CancellationToken::new())
1378            .unwrap();
1379
1380        assert!(matches!(
1381            entry,
1382            WalkEntry::Boundary(BoundaryRecord {
1383                kind: BoundaryKind::Mount,
1384                ..
1385            })
1386        ));
1387    }
1388
1389    #[test]
1390    fn admitted_descriptor_survives_parent_path_swap() {
1391        let temp = TempDir::new().unwrap();
1392        let root = temp.path().join("root");
1393        let moved = temp.path().join("moved");
1394        fs::create_dir_all(root.join("child")).unwrap();
1395        fs::write(root.join("child/safe"), b"safe").unwrap();
1396
1397        let scanner = LinuxPlatformScanner::new();
1398        let mut admission = scanner
1399            .admit_root(
1400                &ScanRoot::new(root.clone()).unwrap(),
1401                &CancellationToken::new(),
1402            )
1403            .unwrap();
1404        fs::rename(&root, &moved).unwrap();
1405        fs::create_dir_all(root.join("child")).unwrap();
1406        fs::write(root.join("child/evil"), b"evil").unwrap();
1407
1408        let child = find_child(&scanner, &mut admission, b"child");
1409        let WalkEntry::Directory(mut opened) = scanner
1410            .inspect_child(&admission.directory, &child, &CancellationToken::new())
1411            .unwrap()
1412        else {
1413            panic!("expected descriptor-relative child directory");
1414        };
1415        let names: Vec<_> = scanner
1416            .enumerate_children(&mut opened.handle, &CancellationToken::new(), limits(64))
1417            .unwrap()
1418            .entries
1419            .into_iter()
1420            .map(|entry| entry.file_name)
1421            .collect();
1422
1423        assert!(names.contains(&NativeName::unix(b"safe".to_vec())));
1424        assert!(!names.contains(&NativeName::unix(b"evil".to_vec())));
1425    }
1426
1427    #[test]
1428    fn admit_root_honors_cancellation() {
1429        let temp = TempDir::new().unwrap();
1430        let cancel = CancellationToken::new();
1431        cancel.cancel();
1432        let error = LinuxPlatformScanner::new()
1433            .admit_root(&ScanRoot::new(temp.path().to_path_buf()).unwrap(), &cancel)
1434            .unwrap_err();
1435        assert!(matches!(error, PlatformError::Cancelled));
1436    }
1437
1438    #[test]
1439    fn file_metadata_reports_hard_link_identity() {
1440        let temp = TempDir::new().unwrap();
1441        fs::write(temp.path().join("file"), b"hello world").unwrap();
1442        fs::hard_link(temp.path().join("file"), temp.path().join("second")).unwrap();
1443
1444        let scanner = LinuxPlatformScanner::new();
1445        let mut admission = scanner
1446            .admit_root(
1447                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1448                &CancellationToken::new(),
1449            )
1450            .unwrap();
1451        let children = scanner
1452            .enumerate_children(
1453                &mut admission.directory,
1454                &CancellationToken::new(),
1455                limits(64),
1456            )
1457            .unwrap();
1458        let inspect = |name: &[u8]| {
1459            let child = children
1460                .entries
1461                .iter()
1462                .find(|entry| entry.file_name == NativeName::unix(name.to_vec()))
1463                .unwrap();
1464            let WalkEntry::File(metadata) = scanner
1465                .inspect_child(&admission.directory, child, &CancellationToken::new())
1466                .unwrap()
1467            else {
1468                panic!("expected file");
1469            };
1470            metadata
1471        };
1472        let first = inspect(b"file");
1473        let second = inspect(b"second");
1474        assert_eq!(first.hard_link_key, second.hard_link_key);
1475        assert_eq!(first.allocated_bytes, second.allocated_bytes);
1476    }
1477
1478    #[test]
1479    fn cancellation_stops_enumeration() {
1480        let temp = TempDir::new().unwrap();
1481        fs::create_dir(temp.path().join("child")).unwrap();
1482        let scanner = LinuxPlatformScanner::new();
1483        let mut admission = scanner
1484            .admit_root(
1485                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1486                &CancellationToken::new(),
1487            )
1488            .unwrap();
1489        let cancel = CancellationToken::new();
1490        cancel.cancel();
1491        let error = scanner
1492            .enumerate_children(&mut admission.directory, &cancel, limits(16))
1493            .unwrap_err();
1494        assert!(matches!(error, PlatformError::Cancelled));
1495    }
1496
1497    #[test]
1498    fn enumeration_continues_at_entry_cap_and_enforces_unfit_byte_cap() {
1499        let temp = TempDir::new().unwrap();
1500        fs::write(temp.path().join("a"), b"a").unwrap();
1501        fs::write(temp.path().join("b"), b"b").unwrap();
1502        let scanner = LinuxPlatformScanner::new();
1503
1504        let mut entry_limited = scanner
1505            .admit_root(
1506                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1507                &CancellationToken::new(),
1508            )
1509            .unwrap();
1510        let first = scanner
1511            .enumerate_children(
1512                &mut entry_limited.directory,
1513                &CancellationToken::new(),
1514                limits(1),
1515            )
1516            .unwrap();
1517        assert_eq!(first.entries.len(), 1);
1518        assert!(!first.end_of_directory);
1519        let second = scanner
1520            .enumerate_children(
1521                &mut entry_limited.directory,
1522                &CancellationToken::new(),
1523                limits(1),
1524            )
1525            .unwrap();
1526        assert_eq!(second.entries.len(), 1);
1527        assert!(second.end_of_directory);
1528
1529        let mut byte_limited = scanner
1530            .admit_root(
1531                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1532                &CancellationToken::new(),
1533            )
1534            .unwrap();
1535        assert!(matches!(
1536            scanner.enumerate_children(
1537                &mut byte_limited.directory,
1538                &CancellationToken::new(),
1539                DirectoryReadLimits {
1540                    max_batch_entries: 16,
1541                    max_batch_bytes: 1,
1542                },
1543            ),
1544            Err(PlatformError::ResourceLimit(_))
1545        ));
1546    }
1547
1548    #[test]
1549    fn multi_batch_continuation_matches_single_batch_without_loss_or_duplicates() {
1550        let temp = TempDir::new().unwrap();
1551        for name in ["alpha", "beta", "gamma", "delta", "epsilon"] {
1552            fs::write(temp.path().join(name), name.as_bytes()).unwrap();
1553        }
1554        let scanner = LinuxPlatformScanner::new();
1555        let root = ScanRoot::new(temp.path().to_path_buf()).unwrap();
1556
1557        let mut single = scanner
1558            .admit_root(&root, &CancellationToken::new())
1559            .unwrap();
1560        let single = scanner
1561            .enumerate_children(&mut single.directory, &CancellationToken::new(), limits(64))
1562            .unwrap();
1563        assert!(single.end_of_directory);
1564
1565        let mut paged = scanner
1566            .admit_root(&root, &CancellationToken::new())
1567            .unwrap();
1568        let mut paged_entries = Vec::new();
1569        let mut batch_count = 0;
1570        loop {
1571            let batch = scanner
1572                .enumerate_children(&mut paged.directory, &CancellationToken::new(), limits(2))
1573                .unwrap();
1574            batch_count += 1;
1575            assert!(!batch.entries.is_empty() || batch.end_of_directory);
1576            paged_entries.extend(batch.entries);
1577            if batch.end_of_directory {
1578                break;
1579            }
1580        }
1581
1582        assert!(batch_count > 1);
1583        assert_eq!(paged_entries, single.entries);
1584    }
1585
1586    #[test]
1587    fn root_mount_identity_comes_from_statx() {
1588        let temp = TempDir::new().unwrap();
1589        let admission = LinuxPlatformScanner::new()
1590            .admit_root(
1591                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1592                &CancellationToken::new(),
1593            )
1594            .unwrap();
1595        assert!(admission.metadata.mount_identity.is_some());
1596    }
1597
1598    #[test]
1599    fn bounded_regular_file_read_succeeds_for_regular_file() {
1600        let temp = TempDir::new().unwrap();
1601        fs::write(temp.path().join("file"), b"payload").unwrap();
1602        let scanner = LinuxPlatformScanner::new();
1603        let admission = scanner
1604            .admit_root(
1605                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1606                &CancellationToken::new(),
1607            )
1608            .unwrap();
1609
1610        let read = read_bound_regular_file(
1611            &scanner,
1612            &admission.directory,
1613            &live_read_request(b"file", 7),
1614            &CancellationToken::new(),
1615        )
1616        .unwrap();
1617
1618        assert_eq!(read.bytes, b"payload");
1619        assert_eq!(read.observed_before.kind, EntryKind::File);
1620        assert_eq!(read.observed_before, read.observed_after);
1621    }
1622
1623    #[test]
1624    fn bounded_regular_file_read_enforces_exact_and_overflow_limits() {
1625        let temp = TempDir::new().unwrap();
1626        fs::write(temp.path().join("file"), b"payload").unwrap();
1627        let scanner = LinuxPlatformScanner::new();
1628        let admission = scanner
1629            .admit_root(
1630                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1631                &CancellationToken::new(),
1632            )
1633            .unwrap();
1634
1635        let exact = read_bound_regular_file(
1636            &scanner,
1637            &admission.directory,
1638            &live_read_request(b"file", 7),
1639            &CancellationToken::new(),
1640        )
1641        .unwrap();
1642        assert_eq!(exact.bytes, b"payload");
1643
1644        let overflow = read_bound_regular_file(
1645            &scanner,
1646            &admission.directory,
1647            &live_read_request(b"file", 6),
1648            &CancellationToken::new(),
1649        )
1650        .unwrap_err();
1651        assert!(matches!(
1652            overflow,
1653            BoundedRegularFileReadError::LimitExceeded {
1654                max_bytes: 6,
1655                observed_logical_bytes,
1656            } if observed_logical_bytes == DecimalU128::new(7)
1657        ));
1658    }
1659
1660    #[test]
1661    fn bounded_regular_file_read_reports_missing_precisely() {
1662        let temp = TempDir::new().unwrap();
1663        let scanner = LinuxPlatformScanner::new();
1664        let admission = scanner
1665            .admit_root(
1666                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1667                &CancellationToken::new(),
1668            )
1669            .unwrap();
1670
1671        let error = read_bound_regular_file(
1672            &scanner,
1673            &admission.directory,
1674            &live_read_request(b"missing", 16),
1675            &CancellationToken::new(),
1676        )
1677        .unwrap_err();
1678
1679        assert_eq!(error, BoundedRegularFileReadError::NotFound);
1680        assert!(error.is_verified_absent());
1681    }
1682
1683    #[test]
1684    fn bounded_regular_file_read_rejects_symlink_without_following() {
1685        let temp = TempDir::new().unwrap();
1686        fs::write(temp.path().join("target"), b"payload").unwrap();
1687        std::os::unix::fs::symlink("target", temp.path().join("link")).unwrap();
1688        let scanner = LinuxPlatformScanner::new();
1689        let admission = scanner
1690            .admit_root(
1691                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1692                &CancellationToken::new(),
1693            )
1694            .unwrap();
1695
1696        let error = read_bound_regular_file(
1697            &scanner,
1698            &admission.directory,
1699            &live_read_request(b"link", 16),
1700            &CancellationToken::new(),
1701        )
1702        .unwrap_err();
1703
1704        assert!(matches!(
1705            error,
1706            BoundedRegularFileReadError::SymlinkOrReparse {
1707                observed_kind: EntryKind::Symlink
1708            }
1709        ));
1710    }
1711
1712    #[test]
1713    fn bounded_regular_file_read_rejects_directory_and_fifo_promptly() {
1714        let temp = TempDir::new().unwrap();
1715        fs::create_dir(temp.path().join("directory")).unwrap();
1716        let fifo = temp.path().join("fifo");
1717        let fifo_cstr = CString::new(fifo.as_os_str().as_bytes()).unwrap();
1718        // SAFETY: pathname is NUL terminated and points to a temp-dir target.
1719        let result = unsafe { libc::mkfifo(fifo_cstr.as_ptr(), 0o600) };
1720        assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error());
1721        assert!(fs::symlink_metadata(&fifo).unwrap().file_type().is_fifo());
1722
1723        let scanner = LinuxPlatformScanner::new();
1724        let admission = scanner
1725            .admit_root(
1726                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1727                &CancellationToken::new(),
1728            )
1729            .unwrap();
1730
1731        let directory_error = read_bound_regular_file(
1732            &scanner,
1733            &admission.directory,
1734            &live_read_request(b"directory", 16),
1735            &CancellationToken::new(),
1736        )
1737        .unwrap_err();
1738        assert!(matches!(
1739            directory_error,
1740            BoundedRegularFileReadError::NotRegular {
1741                observed_kind: EntryKind::Directory
1742            }
1743        ));
1744
1745        let fifo_error = read_bound_regular_file(
1746            &scanner,
1747            &admission.directory,
1748            &live_read_request(b"fifo", 16),
1749            &CancellationToken::new(),
1750        )
1751        .unwrap_err();
1752        assert!(matches!(
1753            fifo_error,
1754            BoundedRegularFileReadError::NotRegular {
1755                observed_kind: EntryKind::Other
1756            }
1757        ));
1758    }
1759
1760    #[test]
1761    fn bounded_regular_file_read_supports_non_utf8_basenames() {
1762        let temp = TempDir::new().unwrap();
1763        let raw_name = b"bad-\xff-name";
1764        let path = temp.path().join(OsStr::from_bytes(raw_name));
1765        fs::write(&path, b"payload").unwrap();
1766        let scanner = LinuxPlatformScanner::new();
1767        let admission = scanner
1768            .admit_root(
1769                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1770                &CancellationToken::new(),
1771            )
1772            .unwrap();
1773
1774        let read = read_bound_regular_file(
1775            &scanner,
1776            &admission.directory,
1777            &live_read_request(raw_name, 16),
1778            &CancellationToken::new(),
1779        )
1780        .unwrap();
1781
1782        assert_eq!(read.bytes, b"payload");
1783    }
1784
1785    #[test]
1786    fn bounded_regular_file_read_checks_expected_binding_before_reading() {
1787        let temp = TempDir::new().unwrap();
1788        fs::write(temp.path().join("file"), b"payload").unwrap();
1789        let scanner = LinuxPlatformScanner::new();
1790        let admission = scanner
1791            .admit_root(
1792                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1793                &CancellationToken::new(),
1794            )
1795            .unwrap();
1796
1797        let live = read_bound_regular_file(
1798            &scanner,
1799            &admission.directory,
1800            &live_read_request(b"file", 16),
1801            &CancellationToken::new(),
1802        )
1803        .unwrap();
1804        let request = BoundedRegularFileReadRequest::previously_observed(
1805            NativeName::unix(b"file".to_vec()),
1806            EntryIdentity::from_unix(
1807                live.observed_before.identity.device(),
1808                u64::try_from(live.observed_before.identity.inode()).unwrap() + 1,
1809            ),
1810            live.observed_before.filesystem_identity.clone(),
1811            live.observed_before.mount_identity.clone(),
1812            16,
1813        )
1814        .unwrap();
1815
1816        let error = read_bound_regular_file(
1817            &scanner,
1818            &admission.directory,
1819            &request,
1820            &CancellationToken::new(),
1821        )
1822        .unwrap_err();
1823
1824        assert!(matches!(
1825            error,
1826            BoundedRegularFileReadError::IdentityMismatch(mismatch)
1827                if mismatch.expected.is_some()
1828        ));
1829    }
1830
1831    #[test]
1832    fn bounded_regular_file_read_uses_retained_parent_not_display_path() {
1833        let temp = TempDir::new().unwrap();
1834        let root = temp.path().join("root");
1835        let moved = temp.path().join("moved");
1836        fs::create_dir(&root).unwrap();
1837        fs::write(root.join("safe"), b"safe").unwrap();
1838
1839        let scanner = LinuxPlatformScanner::new();
1840        let admission = scanner
1841            .admit_root(
1842                &ScanRoot::new(root.clone()).unwrap(),
1843                &CancellationToken::new(),
1844            )
1845            .unwrap();
1846        fs::rename(&root, &moved).unwrap();
1847        fs::create_dir(&root).unwrap();
1848        fs::write(root.join("safe"), b"evil").unwrap();
1849
1850        let read = read_bound_regular_file(
1851            &scanner,
1852            &admission.directory,
1853            &live_read_request(b"safe", 16),
1854            &CancellationToken::new(),
1855        )
1856        .unwrap();
1857
1858        assert_eq!(read.bytes, b"safe");
1859    }
1860
1861    #[test]
1862    fn bounded_regular_file_read_honors_cancellation_before_open() {
1863        let temp = TempDir::new().unwrap();
1864        fs::write(temp.path().join("file"), b"payload").unwrap();
1865        let scanner = LinuxPlatformScanner::new();
1866        let admission = scanner
1867            .admit_root(
1868                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1869                &CancellationToken::new(),
1870            )
1871            .unwrap();
1872        let cancel = CancellationToken::new();
1873        cancel.cancel();
1874
1875        let error = read_bound_regular_file(
1876            &scanner,
1877            &admission.directory,
1878            &live_read_request(b"file", 16),
1879            &cancel,
1880        )
1881        .unwrap_err();
1882
1883        assert_eq!(error, BoundedRegularFileReadError::Cancelled);
1884    }
1885
1886    #[test]
1887    fn bounded_regular_file_read_honors_cancellation_between_reads() {
1888        let temp = TempDir::new().unwrap();
1889        fs::write(
1890            temp.path().join("file"),
1891            vec![b'x'; LinuxPlatformScanner::REGULAR_FILE_READ_CHUNK_BYTES * 2],
1892        )
1893        .unwrap();
1894        let scanner = LinuxPlatformScanner::new();
1895        let admission = scanner
1896            .admit_root(
1897                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1898                &CancellationToken::new(),
1899            )
1900            .unwrap();
1901        let cancel = CancellationToken::new();
1902        let _hook_reset = install_regular_file_read_test_hook(RegularFileReadTestHook {
1903            event: RegularFileReadTestHookEvent::AfterReadChunk,
1904            action: RegularFileReadTestHookAction::Cancel,
1905        });
1906
1907        let error = read_bound_regular_file(
1908            &scanner,
1909            &admission.directory,
1910            &live_read_request(
1911                b"file",
1912                LinuxPlatformScanner::REGULAR_FILE_READ_CHUNK_BYTES * 3,
1913            ),
1914            &cancel,
1915        )
1916        .unwrap_err();
1917
1918        assert_eq!(error, BoundedRegularFileReadError::Cancelled);
1919    }
1920
1921    #[test]
1922    fn bounded_regular_file_read_honors_cancellation_after_metadata() {
1923        let temp = TempDir::new().unwrap();
1924        fs::write(temp.path().join("file"), b"payload").unwrap();
1925        let scanner = LinuxPlatformScanner::new();
1926        let admission = scanner
1927            .admit_root(
1928                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1929                &CancellationToken::new(),
1930            )
1931            .unwrap();
1932        let cancel = CancellationToken::new();
1933        let _hook_reset = install_regular_file_read_test_hook(RegularFileReadTestHook {
1934            event: RegularFileReadTestHookEvent::AfterObservedAfter,
1935            action: RegularFileReadTestHookAction::Cancel,
1936        });
1937
1938        let error = read_bound_regular_file(
1939            &scanner,
1940            &admission.directory,
1941            &live_read_request(b"file", 16),
1942            &cancel,
1943        )
1944        .unwrap_err();
1945
1946        assert_eq!(error, BoundedRegularFileReadError::Cancelled);
1947    }
1948
1949    #[test]
1950    fn bounded_regular_file_read_detects_changed_during_read_with_test_seam() {
1951        let temp = TempDir::new().unwrap();
1952        let path = temp.path().join("file");
1953        fs::write(&path, b"payload").unwrap();
1954        let scanner = LinuxPlatformScanner::new();
1955        let admission = scanner
1956            .admit_root(
1957                &ScanRoot::new(temp.path().to_path_buf()).unwrap(),
1958                &CancellationToken::new(),
1959            )
1960            .unwrap();
1961        let _hook_reset = install_regular_file_read_test_hook(RegularFileReadTestHook {
1962            event: RegularFileReadTestHookEvent::BeforeObservedAfter,
1963            action: RegularFileReadTestHookAction::RewriteBytes {
1964                path,
1965                bytes: b"changed-size".to_vec(),
1966            },
1967        });
1968
1969        let error = read_bound_regular_file(
1970            &scanner,
1971            &admission.directory,
1972            &live_read_request(b"file", 16),
1973            &CancellationToken::new(),
1974        )
1975        .unwrap_err();
1976
1977        assert!(matches!(
1978            error,
1979            BoundedRegularFileReadError::ChangedDuringRead(_)
1980        ));
1981    }
1982}