Skip to main content

reddb_server/storage/engine/
pager.rs

1//! Pager - Page I/O Manager
2//!
3//! The Pager is responsible for reading and writing pages to/from disk.
4//! It integrates with the PageCache for efficient caching and the FreeList
5//! for page allocation.
6//!
7//! # Responsibilities
8//!
9//! 1. **Page I/O**: Read/write 16 KiB pages from/to disk
10//! 2. **Caching**: Integrate with SIEVE PageCache
11//! 3. **Allocation**: Manage free page allocation via FreeList
12//! 4. **Header Management**: Maintain database header (page 0)
13//!
14//! # File Layout
15//!
16//! ```text
17//! ┌─────────────────────────────────────────────────────────────┐
18//! │ Page 0: Database Header                                     │
19//! │   - Magic bytes "RDDB"                                      │
20//! │   - Version                                                 │
21//! │   - Page count                                              │
22//! │   - Freelist head                                           │
23//! ├─────────────────────────────────────────────────────────────┤
24//! │ Page 1: Root B-tree page (or first data page)              │
25//! ├─────────────────────────────────────────────────────────────┤
26//! │ Page 2..N: Data pages                                       │
27//! └─────────────────────────────────────────────────────────────┘
28//! ```
29//!
30//! # References
31//!
32//! - Turso `core/storage/pager.rs:54-134` - HeaderRef::from_pager()
33//! - Turso `core/storage/pager.rs:120` - pager.add_dirty(&page)
34
35#[cfg(test)]
36use super::page::PageType;
37use super::page::{Page, PAGE_SIZE};
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40pub use reddb_engine::storage::pager::{DatabaseHeader, ExtentId, PagerError, PhysicalFileHeader};
41use reddb_engine::storage::pager::{
42    Pager as SharedPager, PagerConfig as SharedPagerConfig, WalBarrier,
43};
44#[cfg(test)]
45use reddb_engine::storage::pager_io::{
46    DWB_ZONE_BYTES, DWB_ZONE_START_PAGE, FIRST_ALLOCATABLE_PAGE,
47};
48use std::fs::{File, OpenOptions};
49#[cfg(test)]
50use std::io::{Read, Seek, SeekFrom, Write};
51use std::path::{Path, PathBuf};
52#[cfg(test)]
53use std::sync::atomic::{AtomicU8, Ordering};
54use std::sync::{Arc, Mutex, RwLock};
55const DEFAULT_CACHE_SIZE: usize = 10_000;
56#[cfg(test)]
57static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
58
59#[cfg(test)]
60#[derive(Clone, Copy)]
61struct PagerWriteFault {
62    offset: Option<u64>,
63    prefix_bytes: usize,
64}
65
66#[cfg(test)]
67fn pager_write_fault() -> &'static Mutex<Option<PagerWriteFault>> {
68    static FAULT: std::sync::OnceLock<Mutex<Option<PagerWriteFault>>> = std::sync::OnceLock::new();
69    FAULT.get_or_init(|| Mutex::new(None))
70}
71
72#[cfg(test)]
73pub(crate) fn test_fail_next_pager_write(offset: Option<u64>, prefix_bytes: usize) {
74    let mut fault = pager_write_fault()
75        .lock()
76        .unwrap_or_else(|poisoned| poisoned.into_inner());
77    assert!(fault.is_none(), "pager write fault is already armed");
78    *fault = Some(PagerWriteFault {
79        offset,
80        prefix_bytes,
81    });
82}
83
84#[cfg(test)]
85fn pager_sync_fault() -> &'static Mutex<Option<usize>> {
86    static FAULT: std::sync::OnceLock<Mutex<Option<usize>>> = std::sync::OnceLock::new();
87    FAULT.get_or_init(|| Mutex::new(None))
88}
89
90#[cfg(test)]
91pub(crate) fn test_fail_pager_sync_after(successful_syncs: usize) {
92    let mut fault = pager_sync_fault()
93        .lock()
94        .unwrap_or_else(|poisoned| poisoned.into_inner());
95    assert!(fault.is_none(), "pager sync fault is already armed");
96    *fault = Some(successful_syncs);
97}
98
99/// Pager configuration
100#[derive(Debug, Clone)]
101pub struct PagerConfig {
102    /// Host guarantees immutable published trees and durable resume counters.
103    pub disk_residency: bool,
104    /// Page cache capacity
105    pub cache_size: usize,
106    /// Whether to open read-only
107    pub read_only: bool,
108    /// Whether to create if not exists
109    pub create: bool,
110    /// Whether to verify checksums on read
111    pub verify_checksums: bool,
112    /// Enable double-write buffer for torn page protection
113    pub double_write: bool,
114    /// Optional encryption key. When set, `Pager::open` writes/reads
115    /// pages through `PageEncryptor` and rejects any DB whose
116    /// encryption-marker disagrees with the supplied key (or its
117    /// absence). When `None`, the pager refuses to open a DB whose
118    /// header carries the `RDBE` encryption marker.
119    pub encryption: Option<crate::storage::encryption::SecureKey>,
120}
121
122impl Default for PagerConfig {
123    fn default() -> Self {
124        Self {
125            disk_residency: false,
126            cache_size: DEFAULT_CACHE_SIZE,
127            read_only: false,
128            create: true,
129            verify_checksums: true,
130            double_write: true,
131            encryption: None,
132        }
133    }
134}
135
136/// Native host adapter for the shared pager.
137pub struct Pager {
138    core: SharedPager<NativePagerFile, NativeWalBarrier>,
139    config: PagerConfig,
140    pub(crate) encryption: Option<(
141        crate::storage::encryption::PageEncryptor,
142        crate::storage::encryption::EncryptionHeader,
143    )>,
144}
145
146#[doc(hidden)]
147pub struct NativePagerFile(File);
148
149#[cfg(test)]
150thread_local! {
151    static PAGER_READ_BYTES: std::cell::Cell<Option<u64>> = const {
152        std::cell::Cell::new(None)
153    };
154}
155
156#[cfg(test)]
157pub(crate) fn measure_pager_read_bytes<T>(operation: impl FnOnce() -> T) -> (T, u64) {
158    PAGER_READ_BYTES.with(|bytes| {
159        assert!(bytes.get().is_none(), "pager read measurement cannot nest");
160        bytes.set(Some(0));
161    });
162    let result = operation();
163    let bytes = PAGER_READ_BYTES.with(|bytes| {
164        bytes
165            .take()
166            .expect("pager read measurement remains active until completion")
167    });
168    (result, bytes)
169}
170
171#[cfg(unix)]
172impl Drop for NativePagerFile {
173    fn drop(&mut self) {
174        // flock belongs to the open file description: a concurrent spawn can
175        // retain it until exec closes the inherited CLOEXEC descriptor. That
176        // descriptor is not a pager owner. Release our fence at the end of the
177        // actual owner's lifetime, rather than waiting for every inherited FD.
178        // File close remains the fallback if the explicit unlock fails.
179        let _ = fs2::FileExt::unlock(&self.0);
180    }
181}
182
183impl reddb_file::VfsFile for NativePagerFile {
184    fn try_clone(&self) -> std::io::Result<Self> {
185        Err(std::io::ErrorKind::Unsupported.into())
186    }
187    fn file_len(&self) -> std::io::Result<u64> {
188        Ok(self.0.metadata()?.len())
189    }
190    fn set_len(&self, len: u64) -> std::io::Result<()> {
191        self.0.set_len(len)
192    }
193    fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
194        #[cfg(test)]
195        {
196            let offset = std::io::Seek::stream_position(&mut self.0)?;
197            let fault = {
198                let mut fault = pager_write_fault()
199                    .lock()
200                    .unwrap_or_else(|poisoned| poisoned.into_inner());
201                if fault.is_some_and(|fault| fault.offset.is_none_or(|target| target == offset)) {
202                    fault.take()
203                } else {
204                    None
205                }
206            };
207            if let Some(fault) = fault {
208                let prefix_bytes = fault.prefix_bytes.min(bytes.len().saturating_sub(1));
209                std::io::Write::write_all(&mut self.0, &bytes[..prefix_bytes])?;
210                return Err(std::io::Error::new(
211                    std::io::ErrorKind::StorageFull,
212                    "injected pager disk full",
213                ));
214            }
215        }
216        std::io::Write::write_all(&mut self.0, bytes)
217    }
218    fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
219        let count = std::io::Read::read(&mut self.0, bytes)?;
220        #[cfg(test)]
221        PAGER_READ_BYTES.with(|bytes| {
222            let Some(current) = bytes.get() else {
223                return;
224            };
225            bytes.set(Some(
226                current
227                    .checked_add(u64::try_from(count).expect("read size fits u64"))
228                    .expect("test pager read byte counter does not overflow"),
229            ));
230        });
231        Ok(count)
232    }
233    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
234        std::io::Seek::seek(&mut self.0, pos)
235    }
236    fn sync_all(&self) -> std::io::Result<()> {
237        #[cfg(test)]
238        {
239            let mut fault = pager_sync_fault()
240                .lock()
241                .unwrap_or_else(|poisoned| poisoned.into_inner());
242            if let Some(remaining) = fault.as_mut() {
243                if *remaining == 0 {
244                    *fault = None;
245                    return Err(std::io::Error::other("injected pager fsync failure"));
246                }
247                *remaining -= 1;
248            }
249        }
250        self.0.sync_all()
251    }
252}
253
254/// Durable ownership callbacks installed only while a fenced epoch is active.
255pub(crate) trait PageOwnershipRecorder: Send + Sync {
256    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()>;
257    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()>;
258    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()>;
259    fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
260        Ok(false)
261    }
262    fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
263        Ok(())
264    }
265}
266
267#[doc(hidden)]
268#[derive(Default)]
269pub struct NativeWalBarrier(
270    RwLock<Option<Arc<Mutex<WalWriter>>>>,
271    RwLock<Option<Arc<dyn PageOwnershipRecorder>>>,
272);
273
274impl NativeWalBarrier {
275    fn ownership_recorder(&self) -> std::io::Result<Option<Arc<dyn PageOwnershipRecorder>>> {
276        Ok(self
277            .1
278            .read()
279            .map_err(|_| std::io::Error::other("ownership recorder lock poisoned"))?
280            .clone())
281    }
282}
283
284impl WalBarrier for NativeWalBarrier {
285    fn validate_page_lsn(&self, lsn: u64, checkpoint_lsn: u64) -> std::io::Result<()> {
286        if lsn <= checkpoint_lsn {
287            return Ok(());
288        }
289        let wal = self
290            .0
291            .read()
292            .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
293            .clone();
294        let durable_lsn = match wal {
295            Some(wal) => wal
296                .lock()
297                .unwrap_or_else(|error| error.into_inner())
298                .durable_lsn(),
299            None => checkpoint_lsn,
300        };
301        if lsn > durable_lsn {
302            return Err(std::io::Error::new(
303                std::io::ErrorKind::InvalidData,
304                format!("page LSN {lsn} exceeds durable WAL boundary {durable_lsn}"),
305            ));
306        }
307        Ok(())
308    }
309
310    fn has_page_ownership_journal(&self) -> std::io::Result<bool> {
311        Ok(self.ownership_recorder()?.is_some())
312    }
313
314    fn needs_page_image(&self, page_id: u32) -> std::io::Result<bool> {
315        self.ownership_recorder()?
316            .map_or(Ok(false), |recorder| recorder.needs_page_image(page_id))
317    }
318
319    fn capture_page_image(&self, page: &Page) -> std::io::Result<()> {
320        if let Some(recorder) = self.ownership_recorder()? {
321            recorder.capture_page_image(page)?;
322        }
323        Ok(())
324    }
325
326    fn flush_until(&self, lsn: u64) -> std::io::Result<()> {
327        let wal = self
328            .0
329            .read()
330            .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
331            .clone();
332        if let Some(wal) = wal {
333            let mut writer = wal.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
334            writer.flush_until(lsn)?;
335        } else if lsn != 0 {
336            return Err(std::io::Error::other(
337                "nonzero page LSN requires a WAL durability barrier",
338            ));
339        }
340        Ok(())
341    }
342
343    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()> {
344        if let Some(recorder) = self.ownership_recorder()? {
345            recorder.reserve_pages(start, count)?;
346        }
347        Ok(())
348    }
349
350    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()> {
351        if let Some(recorder) = self.ownership_recorder()? {
352            recorder.detach_free_pages(pages)?;
353        }
354        Ok(())
355    }
356
357    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()> {
358        if let Some(recorder) = self.ownership_recorder()? {
359            recorder.retire_pages(pages)?;
360        }
361        Ok(())
362    }
363}
364
365impl std::ops::Deref for Pager {
366    type Target = SharedPager<NativePagerFile, NativeWalBarrier>;
367    fn deref(&self) -> &Self::Target {
368        &self.core
369    }
370}
371
372impl reddb_engine::storage::page_store::PageStore for Pager {
373    fn read_page(&self, id: u32) -> Result<Page, PagerError> {
374        self.core.read_page(id)
375    }
376    fn write_page(&self, id: u32, page: Page) -> Result<(), PagerError> {
377        self.core.write_page(id, page)
378    }
379    fn allocate_page(&self, kind: super::page::PageType) -> Result<Page, PagerError> {
380        self.core.allocate_page(kind)
381    }
382    fn free_page(&self, id: u32) -> Result<(), PagerError> {
383        self.core.free_page(id)
384    }
385    fn page_count(&self) -> Result<u32, PagerError> {
386        self.core.page_count()
387    }
388    fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
389        self.core.enable_deferred_reclamation()
390    }
391    fn prefetch_hint(&self, id: u32) {
392        self.prefetch_hint(id)
393    }
394}
395
396#[path = "pager/impl.rs"]
397mod pager_impl;
398impl Drop for Pager {
399    fn drop(&mut self) {
400        // Try to flush on drop
401        let _ = self.flush();
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    #[cfg(target_os = "linux")]
409    use pager_impl::parse_mountinfo_options_for_path;
410    use pager_impl::{
411        classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
412    };
413    use std::fs;
414    use std::io::Write;
415
416    #[cfg(unix)]
417    #[test]
418    fn native_pager_drop_releases_lock_with_inherited_descriptor() {
419        use fs2::FileExt;
420        let directory = tempfile::tempdir().expect("lock fixture");
421        let path = directory.path().join("owner.rdb");
422        let file = std::fs::OpenOptions::new()
423            .read(true)
424            .write(true)
425            .create_new(true)
426            .open(&path)
427            .expect("owner file");
428        file.try_lock_exclusive().expect("owner lock");
429        let owner = NativePagerFile(file);
430        // dup and fork retain the same open file description. A concurrent
431        // spawn may hold this non-owner descriptor until the child execs,
432        // even though Rust opens it with CLOEXEC.
433        let inherited = owner.0.try_clone().expect("inherited descriptor");
434        let contender = std::fs::OpenOptions::new()
435            .read(true)
436            .write(true)
437            .open(&path)
438            .expect("independent contender");
439        assert!(
440            contender.try_lock_exclusive().is_err(),
441            "live owner must exclude a second writer"
442        );
443        drop(owner);
444        contender
445            .try_lock_exclusive()
446            .expect("closed owner must permit immediate reopen, before the child execs");
447        // Releasing the inherited descriptor must not unlock the new owner.
448        drop(inherited);
449        let third = std::fs::OpenOptions::new()
450            .read(true)
451            .write(true)
452            .open(&path)
453            .expect("third handle");
454        assert!(
455            third.try_lock_exclusive().is_err(),
456            "replacement writer retains its independent fence"
457        );
458    }
459
460    fn temp_db_path() -> PathBuf {
461        use std::sync::atomic::{AtomicU64, Ordering};
462        static COUNTER: AtomicU64 = AtomicU64::new(0);
463        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
464        let mut path = std::env::temp_dir();
465        path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
466        path
467    }
468
469    fn cleanup(path: &Path) {
470        // Remove the data file AND every pager shadow sidecar via reddb-file's
471        // authoritative path helpers (the pager writes only these three shadows
472        // next to the data file). Deriving the paths through reddb-file keeps the
473        // file-suffix contract out of the server source (enforced by reddb-file's
474        // `server_source_does_not_embed_owned_file_suffixes`). Tests drop the
475        // pager BEFORE calling this (every call site uses an inner block) so the
476        // drop-time flush cannot re-create a sidecar after removal.
477        let _ = fs::remove_file(path);
478        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
479            let _ = fs::remove_file(sidecar);
480        }
481    }
482
483    fn dwb_path_for(path: &Path) -> PathBuf {
484        reddb_file::layout::pager_dwb_shadow_path(path)
485    }
486
487    fn dwb_zone_offset_for_test() -> u64 {
488        3 * PAGE_SIZE as u64
489    }
490
491    static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
492
493    struct CowAtomicWriteOverrideGuard {
494        _guard: std::sync::MutexGuard<'static, ()>,
495    }
496
497    impl Drop for CowAtomicWriteOverrideGuard {
498        fn drop(&mut self) {
499            COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
500        }
501    }
502
503    fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
504        let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
505            .lock()
506            .unwrap_or_else(|err| err.into_inner());
507        COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
508        CowAtomicWriteOverrideGuard { _guard: guard }
509    }
510
511    fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
512        let pages: Vec<_> = pages
513            .iter()
514            .map(|(page_id, page)| {
515                let mut page = page.clone();
516                page.update_checksum();
517                (*page_id, page)
518            })
519            .collect();
520        let buf = reddb_file::encode_paged_dwb_frame(
521            pages
522                .iter()
523                .map(|(page_id, page)| (*page_id, page.as_bytes())),
524        );
525
526        let mut file = OpenOptions::new()
527            .read(true)
528            .write(true)
529            .open(path)
530            .expect("open() should succeed");
531        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
532            .expect("seek() should succeed");
533        file.write_all(&buf).expect("write_all() should succeed");
534        file.sync_all().expect("sync_all() should succeed");
535    }
536
537    fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
538        let mut file = OpenOptions::new()
539            .write(true)
540            .open(path)
541            .expect("open() should succeed");
542        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
543            .expect("value is present");
544        file.write_all(page.as_bytes())
545            .expect("write_all() should succeed");
546        file.sync_all().expect("sync_all() should succeed");
547    }
548
549    fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
550        let mut torn = *before.as_bytes();
551        torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
552
553        let mut file = OpenOptions::new()
554            .write(true)
555            .open(path)
556            .expect("open() should succeed");
557        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
558            .expect("value is present");
559        file.write_all(&torn).expect("write_all() should succeed");
560        file.sync_all().expect("sync_all() should succeed");
561    }
562
563    #[test]
564    fn test_pager_create_new() {
565        let path = temp_db_path();
566        cleanup(&path);
567
568        {
569            let pager = Pager::open_default(&path).expect("open_default() should succeed");
570            assert_eq!(
571                pager.page_count().expect("page_count() should succeed"),
572                FIRST_ALLOCATABLE_PAGE
573            );
574            // Header + reserved metadata pages + DWB zone
575        }
576
577        cleanup(&path);
578    }
579
580    #[test]
581    fn native_pager_lock_guards_own_io_and_releases_on_drop() {
582        let directory = tempfile::tempdir().expect("directory");
583        let path = directory.path().join("locked-io.rdb");
584        let pager = Pager::open_default(&path).expect("locked pager opens");
585        let mut page = pager
586            .allocate_page(PageType::BTreeLeaf)
587            .expect("allocate under lock");
588        let id = page.page_id();
589        page.insert_cell(b"key", b"kept").expect("cell");
590        pager.write_page(id, page).expect("write under lock");
591        pager.sync().expect("sync under lock");
592        assert_eq!(
593            pager
594                .read_page(id)
595                .expect("read under lock")
596                .read_cell(0)
597                .expect("cell"),
598            (b"key".to_vec(), b"kept".to_vec())
599        );
600        assert!(
601            matches!(Pager::open_default(&path), Err(PagerError::Locked)),
602            "the writer fence remains exclusive"
603        );
604        drop(pager);
605        let reopened = Pager::open_default(&path).expect("last I/O handle releases writer fence");
606        assert_eq!(
607            reopened
608                .read_page(id)
609                .expect("durable page")
610                .read_cell(0)
611                .expect("durable cell"),
612            (b"key".to_vec(), b"kept".to_vec())
613        );
614    }
615
616    #[test]
617    fn test_pager_reopen() {
618        let path = temp_db_path();
619        cleanup(&path);
620
621        // Create and write
622        {
623            let pager = Pager::open_default(&path).expect("open_default() should succeed");
624
625            // Allocate a page
626            let page = pager
627                .allocate_page(PageType::BTreeLeaf)
628                .expect("allocate_page() should succeed");
629            assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
630
631            pager.sync().expect("sync() should succeed");
632        }
633
634        // Reopen and verify
635        {
636            let pager = Pager::open_default(&path).expect("open_default() should succeed");
637            assert_eq!(
638                pager.page_count().expect("page_count() should succeed"),
639                FIRST_ALLOCATABLE_PAGE + 1
640            );
641            // Header + reserved metadata pages + DWB zone + 1 data page
642        }
643
644        cleanup(&path);
645    }
646
647    #[test]
648    fn test_pager_read_write() {
649        let path = temp_db_path();
650        cleanup(&path);
651
652        {
653            let pager = Pager::open_default(&path).expect("open_default() should succeed");
654
655            // Allocate and write
656            let mut page = pager
657                .allocate_page(PageType::BTreeLeaf)
658                .expect("allocate_page() should succeed");
659            let page_id = page.page_id();
660
661            page.insert_cell(b"key", b"value")
662                .expect("insert_cell() should succeed");
663            pager
664                .write_page(page_id, page)
665                .expect("write_page() should succeed");
666
667            // Read back
668            let read_page = pager
669                .read_page(page_id)
670                .expect("read_page() should succeed");
671            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
672            assert_eq!(key, b"key");
673            assert_eq!(value, b"value");
674        }
675
676        cleanup(&path);
677    }
678
679    #[test]
680    fn test_pager_cache() {
681        let path = temp_db_path();
682        cleanup(&path);
683
684        {
685            let pager = Pager::open_default(&path).expect("open_default() should succeed");
686
687            // Allocate a page
688            let page = pager
689                .allocate_page(PageType::BTreeLeaf)
690                .expect("allocate_page() should succeed");
691            let page_id = page.page_id();
692
693            // First read - should be cached from allocate
694            let _ = pager
695                .read_page(page_id)
696                .expect("read_page() should succeed");
697
698            // Second read - should hit cache
699            let _ = pager
700                .read_page(page_id)
701                .expect("read_page() should succeed");
702
703            let stats = pager.cache_stats();
704            assert!(stats.hits >= 1);
705        }
706
707        cleanup(&path);
708    }
709
710    #[test]
711    fn test_pager_free_page() {
712        let path = temp_db_path();
713        cleanup(&path);
714
715        {
716            let pager = Pager::open_default(&path).expect("open_default() should succeed");
717
718            // Allocate pages
719            let page1 = pager
720                .allocate_page(PageType::BTreeLeaf)
721                .expect("allocate_page() should succeed");
722            pager
723                .allocate_page(PageType::BTreeLeaf)
724                .expect("allocate_page() should succeed");
725
726            let id1 = page1.page_id();
727
728            // Free page 1
729            pager.free_page(id1).expect("free_page() should succeed");
730
731            // Next allocation should reuse page 1
732            let page3 = pager
733                .allocate_page(PageType::BTreeLeaf)
734                .expect("allocate_page() should succeed");
735            assert_eq!(page3.page_id(), id1);
736        }
737
738        cleanup(&path);
739    }
740
741    #[test]
742    fn test_freelist_persistence() {
743        let path = temp_db_path();
744        cleanup(&path);
745
746        let freed_id;
747        {
748            let pager = Pager::open_default(&path).expect("open_default() should succeed");
749            let page1 = pager
750                .allocate_page(PageType::BTreeLeaf)
751                .expect("allocate_page() should succeed");
752            let _page2 = pager
753                .allocate_page(PageType::BTreeLeaf)
754                .expect("allocate_page() should succeed");
755            freed_id = page1.page_id();
756
757            pager
758                .free_page(freed_id)
759                .expect("free_page() should succeed");
760            pager.sync().expect("sync() should succeed");
761        }
762
763        {
764            let pager = Pager::open_default(&path).expect("open_default() should succeed");
765            let page = pager
766                .allocate_page(PageType::BTreeLeaf)
767                .expect("allocate_page() should succeed");
768            assert_eq!(page.page_id(), freed_id);
769        }
770
771        cleanup(&path);
772    }
773
774    #[test]
775    fn test_pager_read_only() {
776        let path = temp_db_path();
777        cleanup(&path);
778
779        // Create database
780        {
781            let pager = Pager::open_default(&path).expect("open_default() should succeed");
782            pager.sync().expect("sync() should succeed");
783        }
784
785        // Open read-only
786        {
787            let config = PagerConfig {
788                read_only: true,
789                ..Default::default()
790            };
791
792            let pager = Pager::open(&path, config).expect("open() should succeed");
793            assert!(pager.is_read_only());
794
795            // Should fail to allocate
796            assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
797        }
798
799        cleanup(&path);
800    }
801
802    #[test]
803    fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
804        let path = temp_db_path();
805        cleanup(&path);
806
807        let config = PagerConfig {
808            double_write: true,
809            ..Default::default()
810        };
811
812        let page_id;
813        {
814            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
815            let page = pager
816                .allocate_page(PageType::BTreeLeaf)
817                .expect("allocate_page() should succeed");
818            page_id = page.page_id();
819            pager.sync().expect("sync() should succeed");
820        }
821
822        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
823        recovered_page
824            .insert_cell(b"key", b"value")
825            .expect("insert_cell() should succeed");
826        write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
827
828        assert!(!dwb_path_for(&path).exists());
829
830        {
831            let pager = Pager::open(&path, config).expect("open() should succeed");
832
833            let read_page = pager
834                .read_page(page_id)
835                .expect("read_page() should succeed");
836            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
837            assert_eq!(key, b"key");
838            assert_eq!(value, b"value");
839
840            assert!(!dwb_path_for(&path).exists());
841
842            let mut updated_page = recovered_page.clone();
843            updated_page
844                .insert_cell(b"key2", b"value2")
845                .expect("insert_cell() should succeed");
846            pager
847                .write_page(page_id, updated_page)
848                .expect("write_page() should succeed");
849            pager.flush().expect("flush() should succeed");
850
851            assert!(!dwb_path_for(&path).exists());
852        }
853
854        cleanup(&path);
855    }
856
857    #[test]
858    fn retired_dwb_sidecar_routes_through_offline_migration() {
859        let path = temp_db_path();
860        cleanup(&path);
861
862        {
863            let pager = Pager::open_default(&path).expect("open_default() should succeed");
864            pager.sync().expect("sync() should succeed");
865        }
866
867        let dwb = dwb_path_for(&path);
868        fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
869
870        let err = match Pager::open_default(&path) {
871            Ok(_) => panic!("retired DWB sidecar must refuse open"),
872            Err(err) => err,
873        };
874        assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
875
876        cleanup(&path);
877    }
878
879    #[test]
880    fn interrupted_dwb_staging_write_is_discarded_on_open() {
881        let path = temp_db_path();
882        cleanup(&path);
883
884        {
885            let pager = Pager::open_default(&path).expect("open_default() should succeed");
886            pager.sync().expect("sync() should succeed");
887        }
888
889        let mut file = OpenOptions::new()
890            .read(true)
891            .write(true)
892            .open(&path)
893            .expect("open() should succeed");
894        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
895            .expect("seek() should succeed");
896        file.write_all(b"RDDW")
897            .expect("write corrupt DWB magic should succeed");
898        file.sync_all().expect("sync_all() should succeed");
899
900        {
901            let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
902            pager.sync().expect("sync() should succeed");
903        }
904
905        let mut cleared = [0u8; 4];
906        let mut file = OpenOptions::new()
907            .read(true)
908            .open(&path)
909            .expect("open() should succeed");
910        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
911            .expect("seek() should succeed");
912        file.read_exact(&mut cleared)
913            .expect("read_exact() should succeed");
914        assert_ne!(cleared, reddb_file::DWB_MAGIC);
915
916        cleanup(&path);
917    }
918
919    #[test]
920    fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
921        assert_eq!(
922            classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
923            Some(CowFilesystemKind::Zfs),
924            "ZFS is always CoW"
925        );
926        assert_eq!(
927            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
928            Some(CowFilesystemKind::BtrfsDataCow),
929            "btrfs qualifies only when datacow remains enabled"
930        );
931        assert_eq!(
932            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
933            None,
934            "btrfs nodatacow mount option must reject DWB skip"
935        );
936        assert_eq!(
937            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
938            None,
939            "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
940        );
941        assert_eq!(
942            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
943            None,
944            "missing btrfs inode flags are uncertain and must fail closed"
945        );
946        assert_eq!(
947            classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
948            None,
949            "missing btrfs mount options are uncertain and must fail closed"
950        );
951    }
952
953    #[cfg(target_os = "linux")]
954    #[test]
955    fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
956        let mountinfo = "\
95724 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
95835 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
95936 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
960";
961
962        assert_eq!(
963            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
964                .as_deref(),
965            Some("rw,relatime,rw,space_cache=v2")
966        );
967        assert_eq!(
968            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
969                .as_deref(),
970            Some("rw,relatime,rw,nodatacow")
971        );
972    }
973
974    #[test]
975    fn double_write_false_keeps_dwb_when_cow_probe_denies() {
976        let _override = cow_atomic_write_override(false);
977        let path = temp_db_path();
978        cleanup(&path);
979
980        {
981            let config = PagerConfig {
982                double_write: false,
983                ..Default::default()
984            };
985            let pager = Pager::open(&path, config).expect("open() should succeed");
986            let page = pager
987                .allocate_page(PageType::BTreeLeaf)
988                .expect("allocate_page() should succeed");
989            pager
990                .write_page(page.page_id(), page)
991                .expect("write_page() should succeed");
992            pager.flush().expect("flush() should succeed");
993        }
994
995        assert!(
996            !dwb_path_for(&path).exists(),
997            "DWB must stay enabled in-file when double_write=false is not proven safe"
998        );
999
1000        cleanup(&path);
1001    }
1002
1003    #[test]
1004    fn double_write_false_skips_dwb_when_cow_probe_allows() {
1005        let _override = cow_atomic_write_override(true);
1006        let path = temp_db_path();
1007        cleanup(&path);
1008
1009        {
1010            let config = PagerConfig {
1011                double_write: false,
1012                ..Default::default()
1013            };
1014            let pager = Pager::open(&path, config).expect("open() should succeed");
1015            let page = pager
1016                .allocate_page(PageType::BTreeLeaf)
1017                .expect("allocate_page() should succeed");
1018            pager
1019                .write_page(page.page_id(), page)
1020                .expect("write_page() should succeed");
1021            pager.flush().expect("flush() should succeed");
1022        }
1023
1024        assert!(
1025            !dwb_path_for(&path).exists(),
1026            "DWB may be skipped only after the CoW probe allows it"
1027        );
1028
1029        cleanup(&path);
1030    }
1031
1032    #[test]
1033    fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
1034        let _override = cow_atomic_write_override(true);
1035        let path = temp_db_path();
1036        cleanup(&path);
1037
1038        let page_id;
1039        {
1040            let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1041            let page = pager
1042                .allocate_page(PageType::BTreeLeaf)
1043                .expect("allocate_page() should succeed");
1044            page_id = page.page_id();
1045            pager.sync().expect("sync() should succeed");
1046        }
1047
1048        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
1049        recovered_page
1050            .insert_cell(b"key", b"value")
1051            .expect("insert_cell() should succeed");
1052        write_dwb_fixture(&path, &[(page_id, recovered_page)]);
1053
1054        {
1055            let config = PagerConfig {
1056                double_write: false,
1057                ..Default::default()
1058            };
1059            let pager = Pager::open(&path, config).expect("open() should succeed");
1060            let read_page = pager
1061                .read_page(page_id)
1062                .expect("read_page() should succeed");
1063            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
1064            assert_eq!(key, b"key");
1065            assert_eq!(value, b"value");
1066        }
1067
1068        assert!(!dwb_path_for(&path).exists());
1069
1070        cleanup(&path);
1071    }
1072
1073    #[test]
1074    fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
1075        let _override = cow_atomic_write_override(true);
1076        let path = temp_db_path();
1077        cleanup(&path);
1078
1079        let config = PagerConfig {
1080            double_write: false,
1081            ..Default::default()
1082        };
1083
1084        let page_id;
1085        let before;
1086        let after;
1087        {
1088            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
1089            let mut page = pager
1090                .allocate_page(PageType::BTreeLeaf)
1091                .expect("allocate_page() should succeed");
1092            page_id = page.page_id();
1093            page.insert_cell(b"phase", b"before")
1094                .expect("insert_cell() should succeed");
1095            pager
1096                .write_page(page_id, page)
1097                .expect("write_page() should succeed");
1098            pager.sync().expect("sync() should succeed");
1099            before = pager
1100                .read_page(page_id)
1101                .expect("read_page() should succeed");
1102
1103            let mut page = before.clone();
1104            page.insert_cell(b"phase2", b"after")
1105                .expect("insert_cell() should succeed");
1106            pager
1107                .write_page(page_id, page)
1108                .expect("write_page() should succeed");
1109            pager.flush().expect("flush() should succeed");
1110            after = pager
1111                .read_page(page_id)
1112                .expect("read_page() should succeed");
1113        }
1114
1115        // CoW crash model: the interrupted write leaves either the old full
1116        // page or the new full page, never a torn mix. Exercise both outcomes.
1117        for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
1118            write_page_bytes(&path, page_id, whole_page);
1119
1120            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
1121            let recovered = pager
1122                .read_page(page_id)
1123                .expect("read_page() should succeed");
1124            assert_eq!(recovered.cell_count(), expected_cells);
1125            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
1126            assert_eq!(key, b"phase");
1127            assert_eq!(value, b"before");
1128            if expected_cells == 2 {
1129                let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
1130                assert_eq!(key, b"phase2");
1131                assert_eq!(value, b"after");
1132            }
1133            drop(pager);
1134        }
1135
1136        cleanup(&path);
1137    }
1138
1139    #[test]
1140    fn same_mid_write_without_cow_recovers_from_dwb() {
1141        let _override = cow_atomic_write_override(false);
1142        let path = temp_db_path();
1143        cleanup(&path);
1144
1145        let config = PagerConfig {
1146            double_write: false,
1147            ..Default::default()
1148        };
1149
1150        let page_id;
1151        let before;
1152        let after;
1153        {
1154            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
1155            let mut page = pager
1156                .allocate_page(PageType::BTreeLeaf)
1157                .expect("allocate_page() should succeed");
1158            page_id = page.page_id();
1159            page.insert_cell(b"phase", b"before")
1160                .expect("insert_cell() should succeed");
1161            pager
1162                .write_page(page_id, page)
1163                .expect("write_page() should succeed");
1164            pager.sync().expect("sync() should succeed");
1165            before = pager
1166                .read_page(page_id)
1167                .expect("read_page() should succeed");
1168
1169            let mut page = before.clone();
1170            page.insert_cell(b"phase2", b"after")
1171                .expect("insert_cell() should succeed");
1172            pager
1173                .write_page(page_id, page)
1174                .expect("write_page() should succeed");
1175            pager.flush().expect("flush() should succeed");
1176            after = pager
1177                .read_page(page_id)
1178                .expect("read_page() should succeed");
1179        }
1180
1181        write_dwb_fixture(&path, &[(page_id, after.clone())]);
1182        write_torn_page_bytes(&path, page_id, &before, &after);
1183
1184        {
1185            let pager = Pager::open(&path, config).expect("open() should succeed");
1186            let recovered = pager
1187                .read_page(page_id)
1188                .expect("read_page() should succeed");
1189            assert_eq!(recovered.cell_count(), 2);
1190
1191            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
1192            assert_eq!(key, b"phase");
1193            assert_eq!(value, b"before");
1194
1195            let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
1196            assert_eq!(key, b"phase2");
1197            assert_eq!(value, b"after");
1198        }
1199
1200        assert!(!dwb_path_for(&path).exists());
1201        cleanup(&path);
1202    }
1203
1204    // -----------------------------------------------------------------
1205    // Target 3: WAL-first flush ordering
1206    // -----------------------------------------------------------------
1207
1208    #[test]
1209    fn pager_starts_without_wal_writer() {
1210        let path = temp_db_path();
1211        let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1212        assert!(!pager.has_wal_writer());
1213        drop(pager);
1214        cleanup(&path);
1215    }
1216
1217    #[test]
1218    fn flush_refuses_nonzero_lsn_without_a_wal_writer() {
1219        let path = temp_db_path();
1220        let pager = Pager::open(&path, PagerConfig::default()).expect("open pager");
1221        let mut page = pager
1222            .allocate_page(PageType::BTreeLeaf)
1223            .expect("allocate page");
1224        page.set_lsn(42);
1225        pager.write_page(page.page_id(), page).expect("write page");
1226
1227        assert!(
1228            pager.flush().is_err(),
1229            "a page with an LSN needs a WAL barrier"
1230        );
1231        assert!(
1232            pager.dirty_page_count() > 0,
1233            "failed flush must retain dirty pages"
1234        );
1235        assert!(
1236            pager.flush().is_err(),
1237            "failed writeback must remain fenced"
1238        );
1239        drop(pager);
1240        cleanup(&path);
1241    }
1242
1243    #[test]
1244    fn set_wal_writer_attaches_handle() {
1245        use crate::storage::wal::writer::WalWriter;
1246        use std::sync::{Arc, Mutex};
1247
1248        let db_path = temp_db_path();
1249        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1250        let _ = fs::remove_file(&wal_path);
1251
1252        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1253        let wal = Arc::new(Mutex::new(
1254            WalWriter::open(&wal_path).expect("open() should succeed"),
1255        ));
1256        pager.set_wal_writer(Arc::clone(&wal));
1257        assert!(pager.has_wal_writer());
1258
1259        pager.clear_wal_writer();
1260        assert!(!pager.has_wal_writer());
1261
1262        drop(pager);
1263        let _ = fs::remove_file(&wal_path);
1264        cleanup(&db_path);
1265    }
1266
1267    #[test]
1268    fn flush_with_lsn_zero_pages_skips_wal_call() {
1269        // When every dirty page has lsn == 0 (the legacy auto-commit
1270        // path), flush() must NOT call wal.flush_until — there is no
1271        // WAL record to wait for. We verify this by attaching a WAL
1272        // whose durable_lsn starts at 8 and confirming flush() does
1273        // not advance it (no append, no flush).
1274        use crate::storage::wal::writer::WalWriter;
1275        use std::sync::{Arc, Mutex};
1276
1277        let db_path = temp_db_path();
1278        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1279        let _ = fs::remove_file(&wal_path);
1280
1281        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1282        let wal = Arc::new(Mutex::new(
1283            WalWriter::open(&wal_path).expect("open() should succeed"),
1284        ));
1285        let initial_durable = {
1286            let g = wal.lock().expect("lock() should succeed");
1287            g.durable_lsn()
1288        };
1289        pager.set_wal_writer(Arc::clone(&wal));
1290
1291        // Allocate and write a page with lsn = 0.
1292        let mut page = pager
1293            .allocate_page(PageType::BTreeLeaf)
1294            .expect("allocate_page() should succeed");
1295        page.insert_cell(b"k", b"v")
1296            .expect("insert_cell() should succeed");
1297        // header.lsn stays at 0 — caller did not stamp.
1298        pager
1299            .write_page(page.page_id(), page)
1300            .expect("write_page() should succeed");
1301        pager.flush().expect("flush() should succeed");
1302
1303        // WAL durable_lsn must be unchanged because flush_until was
1304        // never called (max lsn over dirty pages was 0).
1305        let after_flush = {
1306            let g = wal.lock().expect("lock() should succeed");
1307            g.durable_lsn()
1308        };
1309        assert_eq!(after_flush, initial_durable);
1310
1311        drop(pager);
1312        let _ = fs::remove_file(&wal_path);
1313        cleanup(&db_path);
1314    }
1315
1316    #[test]
1317    fn flush_advances_wal_durable_when_pages_carry_lsn() {
1318        // The full WAL-first dance: append a record, capture the
1319        // returned LSN, stamp it on a page, flush — afterwards the
1320        // WAL must be durable up to at least that LSN.
1321        use crate::storage::wal::record::WalRecord;
1322        use crate::storage::wal::writer::WalWriter;
1323        use std::sync::{Arc, Mutex};
1324
1325        let db_path = temp_db_path();
1326        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1327        let _ = fs::remove_file(&wal_path);
1328
1329        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1330        let wal = Arc::new(Mutex::new(
1331            WalWriter::open(&wal_path).expect("open() should succeed"),
1332        ));
1333        pager.set_wal_writer(Arc::clone(&wal));
1334
1335        // Stamp two dirty pages with a real WAL LSN.
1336        let stamped_lsn = {
1337            let mut wal_guard = wal.lock().expect("lock() should succeed");
1338            wal_guard
1339                .append(&WalRecord::Begin { tx_id: 1 })
1340                .expect("append() should succeed");
1341            wal_guard
1342                .append(&WalRecord::Commit { tx_id: 1 })
1343                .expect("append() should succeed");
1344            wal_guard.current_lsn()
1345        };
1346        let mut page = pager
1347            .allocate_page(PageType::BTreeLeaf)
1348            .expect("allocate_page() should succeed");
1349        page.insert_cell(b"k", b"v")
1350            .expect("insert_cell() should succeed");
1351        // Use the public Page API to set the LSN.
1352        page.set_lsn(stamped_lsn);
1353        let page_id = page.page_id();
1354        pager
1355            .write_page(page_id, page)
1356            .expect("write_page() should succeed");
1357        pager.flush().expect("flush() should succeed");
1358
1359        // After flush, the WAL is durable at least up to our stamp.
1360        let after_flush = {
1361            let g = wal.lock().expect("lock() should succeed");
1362            g.durable_lsn()
1363        };
1364        assert!(
1365            after_flush >= stamped_lsn,
1366            "after flush durable_lsn {} must be >= stamped {}",
1367            after_flush,
1368            stamped_lsn
1369        );
1370
1371        drop(pager);
1372        let pager = Pager::open(&db_path, PagerConfig::default()).expect("cold reopen");
1373        pager.set_wal_writer(Arc::clone(&wal));
1374        assert_eq!(
1375            pager
1376                .read_page(page_id)
1377                .expect("durable WAL authorizes cold page")
1378                .lsn(),
1379            stamped_lsn
1380        );
1381        drop(pager);
1382        let _ = fs::remove_file(&wal_path);
1383        cleanup(&db_path);
1384    }
1385
1386    // -----------------------------------------------------------------
1387    // gh-892: filesystem block-size alignment diagnostic
1388    // -----------------------------------------------------------------
1389
1390    #[test]
1391    fn block_size_warn_fires_for_mismatched_block_size() {
1392        // A block size that does not divide the 16 KiB page size means a
1393        // page write straddles FS blocks — the predicate must report a
1394        // misalignment so `open()` emits the warning.
1395        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1396        // Block larger than the page (e.g. 1 MiB): 16384 % 1048576 != 0.
1397        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1398        // 6 KiB also fails to divide 16 KiB.
1399        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1400    }
1401
1402    #[test]
1403    fn block_size_silent_for_divisor() {
1404        // Block sizes that evenly divide the page size: no straddle, no warn.
1405        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1406        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1407        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1408        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1409    }
1410
1411    #[test]
1412    fn block_size_unavailable_is_silent() {
1413        // st_blksize == 0 means the probe is unavailable; never warn on it.
1414        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1415    }
1416
1417    #[test]
1418    fn page_size_is_unchanged_16kib() {
1419        // The diagnostic must never alter the compile-time page size.
1420        assert_eq!(PAGE_SIZE, 16 * 1024);
1421    }
1422}