Repository: cberner/redb
Pull request #369 (2022-09-21 10:39:36)

Add initial Windows support
1. Windows requires that file mappings to files on disk have a length > 0. This means that several tests immediately fail since they start with an empty file.
2. The Unix implementation truncates files to both decrease and increase the size of the mapped memory and then remaps and asserts that the base pointer is the same so that files can be resized while also being read from concurrently. While it's _possible_ that increasing the file size on Windows can be accomplished with [some "lightly" documented internal kernel functions](https://stackoverflow.com/a/55068177), _decreasing_ the size of the file is (AFAIK) impossible without unmapping the file first.

This provides initial support for Windows that passes _almost_ all tests, I've disabled `regression14 + 15` tests since they both allocate huge files and Windows since it doesn't support dynamic resize.

The only substantial change, other than just adding the mmap support for windows itself, was to change `TransactionalMemory::new` a bit so that it sizes the file appropriately _before_ opening the Mmap so that both Unix and Windows can be supported transparently. Since resizing is not supported the `dynamic_growth` toggle is always turned off on Windows with no way to enable it via the public API. I didn't go the more extreme route of removing the resize method from Mmap on Windows, since as I said above, it _could_ be possible to enable at least dynamic _increasing_ growth if wanted, but not be able to decrease it, but if that was wanted that could be moved to a separate PR as a follow up. Also allowing dynamic increasing growth would allow regression14 + 15 to be turned back on since they wouldn't hit disk limits.

There were also a few minor fixups to align the errors expected by tests when running on Windows.

Resolves: #300 

cberner: Cool, thanks for working on this!

1. Should be easy to fix. Take a look at `TransactionalMemory::new` I think if you just refractor it a bit you can pass the initial size to `Mmap` instead of needing to call resize()
2. This one is annoying, I think you can assert that `dynamic_growth` is false, and make that the default on Windows. That should disable the growing and shrinking logic 
Jake-Shadle: Ahh ok, I initially didn't want to touch anything outside of the mmap stuff itself to avoid making the PR bigger than it already is, I'll take a look at those suggestions tomorrow.
Jake-Shadle: I'm not sure what the mmap leak test is for, but at least on the M1 I was running tests on it took ~20ms to do the mmap lock + construction which mean the 100000 iterations took a while... I also saw a test failure in the `reload_mmap` test, but I don't think that was because of this PR.
cberner: The mmap leak test is to verify that it calls munmap. I had a bug at one point where it didn't, and then the fuzzer found that it ran out of address space after creating a few thousand Database objects and closing them :)

Patch:
diff --git a/src/db.rs b/src/db.rs
index 15a16f8..2d87736 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -166,7 +166,7 @@ impl Database {
     pub unsafe fn create(path: impl AsRef<Path>, db_size: usize) -> Result<Database> {
         let db_size = db_size as u64;
         let file = if path.as_ref().exists() && File::open(path.as_ref())?.metadata()?.len() > 0 {
-            let existing_size = get_db_size(path.as_ref())?;
+            let existing_size = Self::get_db_size(path.as_ref())?;
             if existing_size != db_size {
                 return Err(Error::DbSizeMismatch {
                     path: path.as_ref().to_string_lossy().to_string(),
@@ -188,7 +188,7 @@ impl Database {
             db_size,
             None,
             None,
-            true,
+            cfg!(unix),
             Some(WriteStrategy::default()),
         )
     }
@@ -199,15 +199,38 @@ impl Database {
     ///
     /// The file referenced by `path` must not be concurrently modified by any other process
     pub unsafe fn open(path: impl AsRef<Path>) -> Result<Database> {
-        if File::open(path.as_ref())?.metadata()?.len() > 0 {
-            let existing_size = get_db_size(path.as_ref())?;
+        if !path.as_ref().exists() {
+            Err(Error::Io(ErrorKind::NotFound.into()))
+        } else if File::open(path.as_ref())?.metadata()?.len() > 0 {
+            let existing_size = Self::get_db_size(path.as_ref())?;
             let file = OpenOptions::new().read(true).write(true).open(path)?;
-            Database::new(file, existing_size, None, None, true, None)
+            Database::new(file, existing_size, None, None, cfg!(unix), None)
         } else {
             Err(Error::Io(io::Error::from(ErrorKind::InvalidData)))
         }
     }
 
+    #[inline]
+    fn get_db_size(path: impl AsRef<Path>) -> Result<u64> {
+        #[cfg(unix)]
+        {
+            Ok(get_db_size(path)?)
+        }
+        #[cfg(windows)]
+        {
+            get_db_size(path).map_err(|err| {
+                // On Windows, an exclusive file lock also applies to reads, unlike on Unix, so
+                // we detect that specific error code and transform it to the correct error
+                // 0x21 - ERROR_LOCK_VIOLATION
+                if matches!(err.raw_os_error(), Some(0x21)) {
+                    Error::DatabaseAlreadyOpen
+                } else {
+                    err.into()
+                }
+            })
+        }
+    }
+
     pub(crate) fn get_memory(&self) -> &TransactionalMemory {
         &self.mem
     }
@@ -503,7 +526,7 @@ impl Builder {
         Self {
             page_size: None,
             region_size: None,
-            dynamic_growth: true,
+            dynamic_growth: cfg!(unix),
             write_strategy: WriteStrategy::default(),
         }
     }
@@ -535,6 +558,7 @@ impl Builder {
     /// Whether to grow the database file dynamically.
     /// When set to true, the database file will start at a small size and grow as insertions are made
     /// When set to false, the database file will be statically sized
+    #[cfg(unix)]
     pub fn set_dynamic_growth(&mut self, enabled: bool) -> &mut Self {
         self.dynamic_growth = enabled;
         self
@@ -567,3 +591,10 @@ impl Builder {
         )
     }
 }
+
+// This just makes it easier to throw `dbg` etc statements on `Result<Database>`
+impl std::fmt::Debug for Database {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Database").finish()
+    }
+}
diff --git a/src/tree_store/page_store/buddy_allocator.rs b/src/tree_store/page_store/buddy_allocator.rs
index 7d59d7a..20f0889 100644
--- a/src/tree_store/page_store/buddy_allocator.rs
+++ b/src/tree_store/page_store/buddy_allocator.rs
@@ -85,13 +85,11 @@ impl<'a> BuddyAllocator<'a> {
         Self { data }
     }
 
+    #[inline]
     pub(crate) fn highest_free_order(&self) -> Option<usize> {
-        for order in (0..=self.get_max_order()).rev() {
-            if self.get_order(order as u32).has_unset() {
-                return Some(order);
-            }
-        }
-        None
+        (0..=self.get_max_order())
+            .rev()
+            .find(|order| self.get_order(*order as u32).has_unset())
     }
 
     pub(crate) fn count_free_pages(&self) -> usize {
diff --git a/src/tree_store/page_store/mmap.rs b/src/tree_store/page_store/mmap.rs
index 05718ec..bafa8de 100644
--- a/src/tree_store/page_store/mmap.rs
+++ b/src/tree_store/page_store/mmap.rs
@@ -3,43 +3,25 @@ use std::fs::File;
 use std::io;
 use std::io::ErrorKind;
 use std::ops::Range;
-use std::os::unix::io::AsRawFd;
+use std::slice;
 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
-use std::{ptr, slice};
 
-struct FileLock {
-    fd: libc::c_int,
-}
+#[cfg(unix)]
+mod unix;
+#[cfg(unix)]
+use unix::*;
 
-impl FileLock {
-    fn new(fd: libc::c_int) -> Result<Self> {
-        let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
-        if result != 0 {
-            let err = io::Error::last_os_error();
-            if err.kind() == io::ErrorKind::WouldBlock {
-                Err(Error::DatabaseAlreadyOpen)
-            } else {
-                Err(Error::Io(err))
-            }
-        } else {
-            Ok(Self { fd })
-        }
-    }
-}
-
-impl Drop for FileLock {
-    fn drop(&mut self) {
-        unsafe { libc::flock(self.fd, libc::LOCK_UN) };
-    }
-}
+#[cfg(windows)]
+mod windows;
+#[cfg(windows)]
+use windows::*;
 
 pub(crate) struct Mmap {
     file: File,
     _lock: FileLock,
-    mmap: *mut u8,
+    mmap: MmapInner,
     len: AtomicUsize,
     fsync_failed: AtomicBool,
-    capacity: usize,
 }
 
 // mmap() is documented as being multi-thread safe
@@ -48,128 +30,97 @@ unsafe impl Sync for Mmap {}
 
 impl Mmap {
     pub(crate) fn new(file: File, max_capacity: usize) -> Result<Self> {
-        let len = file.metadata()?.len();
-        assert!(len <= max_capacity as u64);
-        let lock = FileLock::new(file.as_raw_fd())?;
-        let mmap = unsafe {
-            libc::mmap(
-                ptr::null_mut(),
-                max_capacity as libc::size_t,
-                libc::PROT_READ | libc::PROT_WRITE,
-                libc::MAP_SHARED,
-                file.as_raw_fd(),
-                0,
-            )
-        };
-        if mmap == libc::MAP_FAILED {
-            return Err(io::Error::last_os_error().into());
-        }
+        let len = Self::get_valid_length(&file, max_capacity)?;
+        let lock = FileLock::new(&file)?;
+
+        let mmap = MmapInner::create_mapping(&file, len, max_capacity)?;
 
-        let result = Self {
-            mmap: mmap as *mut u8,
+        let mapping = Self {
             file,
             _lock: lock,
+            mmap,
             len: AtomicUsize::new(len as usize),
             fsync_failed: AtomicBool::new(false),
-            capacity: max_capacity,
         };
-        // fsync to ensure that the data we read during initialization is stored durably
-        result.flush()?;
 
-        Ok(result)
+        mapping.flush()?;
+
+        Ok(mapping)
     }
 
-    fn check_fsync_failure(&self) -> Result<()> {
-        if self.fsync_failed.load(Ordering::Acquire) {
-            Err(Error::Io(io::Error::from(ErrorKind::Other)))
+    /// Retrieves the length of the specified file and validates that it is <=
+    /// the maximum capacity the file is allowed to support
+    #[inline]
+    pub(crate) fn get_valid_length(file: &File, max_capacity: usize) -> Result<u64> {
+        let len = file.metadata()?.len();
+
+        if len > max_capacity as u64 {
+            // Unfortunately io::ErrorKind::FileTooLarge is unstable, so we just
+            // cheat here instead and provide the os specific codes
+            let code = if cfg!(target_os = "windows") {
+                0xdf // ERROR_FILE_TOO_LARGE
+            } else {
+                assert!(cfg!(unix), "unsupported target platform");
+                libc::EFBIG
+            };
+            Err(Error::Io(io::Error::from_raw_os_error(code)))
         } else {
-            Ok(())
+            Ok(len)
         }
     }
 
+    #[inline]
     pub(crate) fn len(&self) -> usize {
         self.len.load(Ordering::Acquire)
     }
 
-    // Safety: if new_len < len(), caller must ensure that no references to memory in new_len..len() exist
+    /// SAFETY: if `new_len < len()`, caller must ensure that no references to
+    /// memory in `new_len..len()` exist
     pub(crate) unsafe fn resize(&self, new_len: usize) -> Result<()> {
-        assert!(new_len <= self.capacity);
+        assert!(new_len <= self.mmap.capacity);
         self.check_fsync_failure()?;
-        self.file.set_len(new_len as u64)?;
-
-        let mmap = libc::mmap(
-            self.mmap as *mut libc::c_void,
-            self.capacity as libc::size_t,
-            libc::PROT_READ | libc::PROT_WRITE,
-            libc::MAP_SHARED | libc::MAP_FIXED,
-            self.file.as_raw_fd(),
-            0,
-        );
-
-        if mmap == libc::MAP_FAILED {
-            Err(io::Error::last_os_error().into())
+
+        self.mmap.resize(new_len as u64, self)?;
+
+        self.len.store(new_len, Ordering::Release);
+        Ok(())
+    }
+
+    #[inline]
+    fn check_fsync_failure(&self) -> Result<()> {
+        if self.fsync_failed.load(Ordering::Acquire) {
+            Err(Error::Io(io::Error::from(ErrorKind::Other)))
         } else {
-            assert_eq!(mmap as *mut u8, self.mmap);
-            self.len.store(new_len, Ordering::Release);
             Ok(())
         }
     }
 
-    #[cfg(not(target_os = "macos"))]
-    pub(crate) fn flush(&self) -> Result {
-        self.check_fsync_failure()?;
-        // Disable fsync when fuzzing, since it doesn't test crash consistency
-        #[cfg(not(fuzzing))]
-        {
-            let result = unsafe {
-                libc::msync(
-                    self.mmap as *mut libc::c_void,
-                    self.len() as libc::size_t,
-                    libc::MS_SYNC,
-                )
-            };
-            if result != 0 {
-                self.fsync_failed.store(true, Ordering::Release);
-                return Err(io::Error::last_os_error().into());
-            }
-        }
-        Ok(())
+    #[inline]
+    fn set_fsync_failed(&self, failed: bool) {
+        self.fsync_failed.store(failed, Ordering::Release);
     }
 
-    #[cfg(target_os = "macos")]
-    pub(crate) fn flush(&self) -> Result {
+    #[inline]
+    pub(crate) fn flush(&self) -> Result<()> {
         self.check_fsync_failure()?;
-        #[cfg(not(fuzzing))]
-        {
-            let code = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_FULLFSYNC) };
-            if code == -1 {
-                self.fsync_failed.store(true, Ordering::Release);
-                return Err(io::Error::last_os_error().into());
-            }
+
+        let res = self.mmap.flush(self);
+        if res.is_err() {
+            self.set_fsync_failed(true);
         }
-        Ok(())
-    }
 
-    #[cfg(not(target_os = "macos"))]
-    pub(crate) fn eventual_flush(&self) -> Result {
-        self.check_fsync_failure()?;
-        self.flush()
+        res
     }
 
-    #[cfg(target_os = "macos")]
+    #[inline]
     pub(crate) fn eventual_flush(&self) -> Result {
         self.check_fsync_failure()?;
-        // TODO: It may be unsafe to mix F_BARRIERFSYNC with writes to the mmap.
-        //       Investigate switching to `write()`
-        #[cfg(not(fuzzing))]
-        {
-            let code = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_BARRIERFSYNC) };
-            if code == -1 {
-                self.fsync_failed.store(true, Ordering::Release);
-                return Err(io::Error::last_os_error().into());
-            }
+        let res = self.mmap.eventual_flush(self);
+        if res.is_err() {
+            self.set_fsync_failed(true);
         }
-        Ok(())
+
+        res
     }
 
     // Safety: caller must ensure that [start, end) does not alias any existing references returned
@@ -179,7 +130,7 @@ impl Mmap {
         // TODO: propagate the error
         self.check_fsync_failure()
             .expect("fsync previously failed. Connection closed");
-        let ptr = self.mmap.add(range.start);
+        let ptr = self.mmap.mmap.add(range.start);
         slice::from_raw_parts(ptr, range.len())
     }
 
@@ -191,32 +142,7 @@ impl Mmap {
         // TODO: propagate the error
         self.check_fsync_failure()
             .expect("fsync previously failed. Connection closed");
-        let ptr = self.mmap.add(range.start);
+        let ptr = self.mmap.mmap.add(range.start);
         slice::from_raw_parts_mut(ptr, range.len())
     }
 }
-
-impl Drop for Mmap {
-    fn drop(&mut self) {
-        unsafe {
-            libc::munmap(
-                self.mmap as *mut libc::c_void,
-                self.capacity as libc::size_t,
-            );
-        }
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use crate::tree_store::page_store::mmap::Mmap;
-    use tempfile::NamedTempFile;
-
-    #[test]
-    fn leak() {
-        for _ in 0..100_000 {
-            let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
-            Mmap::new(tmpfile.into_file(), 1024 * 1024).unwrap();
-        }
-    }
-}
diff --git a/src/tree_store/page_store/mmap/unix.rs b/src/tree_store/page_store/mmap/unix.rs
new file mode 100644
index 0000000..05ae922
--- /dev/null
+++ b/src/tree_store/page_store/mmap/unix.rs
@@ -0,0 +1,157 @@
+use super::*;
+use std::os::unix::io::AsRawFd;
+use std::ptr;
+
+pub(super) struct FileLock {
+    fd: libc::c_int,
+}
+
+impl FileLock {
+    pub(super) fn new(file: &File) -> Result<Self> {
+        let fd = file.as_raw_fd();
+        let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
+        if result != 0 {
+            let err = io::Error::last_os_error();
+            if err.kind() == io::ErrorKind::WouldBlock {
+                Err(Error::DatabaseAlreadyOpen)
+            } else {
+                Err(Error::Io(err))
+            }
+        } else {
+            Ok(Self { fd })
+        }
+    }
+}
+
+impl Drop for FileLock {
+    fn drop(&mut self) {
+        unsafe { libc::flock(self.fd, libc::LOCK_UN) };
+    }
+}
+
+pub(super) struct MmapInner {
+    pub(super) mmap: *mut u8,
+    pub(super) capacity: usize,
+}
+
+impl MmapInner {
+    pub(super) fn create_mapping(file: &File, _len: u64, max_capacity: usize) -> Result<Self> {
+        let mmap = unsafe {
+            libc::mmap(
+                ptr::null_mut(),
+                max_capacity as libc::size_t,
+                libc::PROT_READ | libc::PROT_WRITE,
+                libc::MAP_SHARED,
+                file.as_raw_fd(),
+                0,
+            )
+        };
+        if mmap == libc::MAP_FAILED {
+            Err(io::Error::last_os_error().into())
+        } else {
+            Ok(Self {
+                mmap: mmap as *mut u8,
+                capacity: max_capacity,
+            })
+        }
+    }
+
+    /// Safety: if new_len < len(), caller must ensure that no references to memory in new_len..len() exist
+    #[inline]
+    pub(super) unsafe fn resize(&self, new_len: u64, owner: &Mmap) -> Result<()> {
+        owner.file.set_len(new_len)?;
+
+        let mmap = libc::mmap(
+            self.mmap as *mut libc::c_void,
+            self.capacity as libc::size_t,
+            libc::PROT_READ | libc::PROT_WRITE,
+            libc::MAP_SHARED | libc::MAP_FIXED,
+            owner.file.as_raw_fd(),
+            0,
+        );
+
+        if mmap == libc::MAP_FAILED {
+            Err(io::Error::last_os_error().into())
+        } else {
+            assert_eq!(mmap as *mut u8, self.mmap);
+            Ok(())
+        }
+    }
+
+    #[inline]
+    pub(super) fn flush(&self, owner: &Mmap) -> Result {
+        // Disable fsync when fuzzing, since it doesn't test crash consistency
+        #[cfg(not(fuzzing))]
+        {
+            #[cfg(not(target_os = "macos"))]
+            {
+                let result = unsafe {
+                    libc::msync(
+                        self.mmap as *mut libc::c_void,
+                        owner.len() as libc::size_t,
+                        libc::MS_SYNC,
+                    )
+                };
+                if result != 0 {
+                    return Err(io::Error::last_os_error().into());
+                }
+            }
+            #[cfg(target_os = "macos")]
+            {
+                let code = unsafe { libc::fcntl(owner.file.as_raw_fd(), libc::F_FULLFSYNC) };
+                if code == -1 {
+                    return Err(io::Error::last_os_error().into());
+                }
+            }
+        }
+        Ok(())
+    }
+
+    #[inline]
+    pub(super) fn eventual_flush(&self, owner: &Mmap) -> Result {
+        #[cfg(not(target_os = "macos"))]
+        {
+            self.flush(owner)
+        }
+        #[cfg(all(target_os = "macos", not(fuzzing)))]
+        {
+            // TODO: It may be unsafe to mix F_BARRIERFSYNC with writes to the mmap.
+            //       Investigate switching to `write()`
+            let code = unsafe { libc::fcntl(owner.file.as_raw_fd(), libc::F_BARRIERFSYNC) };
+            if code == -1 {
+                Err(io::Error::last_os_error().into())
+            } else {
+                Ok(())
+            }
+        }
+    }
+}
+
+impl Drop for MmapInner {
+    fn drop(&mut self) {
+        unsafe {
+            libc::munmap(
+                self.mmap as *mut libc::c_void,
+                self.capacity as libc::size_t,
+            );
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn leak() {
+        use crate::tree_store::page_store::mmap::Mmap;
+        use tempfile::NamedTempFile;
+
+        for _ in 0..if cfg!(target_os = "macos") {
+            100
+        } else {
+            100_000
+        } {
+            let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
+            Mmap::new(tmpfile.into_file(), 1024 * 1024).unwrap();
+        }
+    }
+}
diff --git a/src/tree_store/page_store/mmap/windows.rs b/src/tree_store/page_store/mmap/windows.rs
new file mode 100644
index 0000000..6ccbd90
--- /dev/null
+++ b/src/tree_store/page_store/mmap/windows.rs
@@ -0,0 +1,271 @@
+#![allow(clippy::upper_case_acronyms)]
+
+use super::*;
+use std::ffi::c_void;
+use std::os::windows::io::AsRawHandle;
+use std::os::windows::io::RawHandle;
+use std::ptr;
+
+#[repr(C)]
+struct OVERLAPPED {
+    internal: usize,
+    internal_high: usize,
+    anonymous: OVERLAPPED_0,
+    event: RawHandle,
+}
+
+#[repr(C)]
+union OVERLAPPED_0 {
+    pub anonymous: OVERLAPPED_0_0,
+    pub pointer: *mut c_void,
+}
+
+#[derive(Copy, Clone)]
+#[repr(C)]
+struct OVERLAPPED_0_0 {
+    offset: u32,
+    offset_high: u32,
+}
+
+const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x00000002;
+const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x00000001;
+const ERROR_IO_PENDING: i32 = 997;
+const PAGE_READWRITE: u32 = 0x4;
+
+const STANDARD_RIGHTS_REQUIRED: u32 = 0x000f0000;
+
+const SECTION_QUERY: u32 = 0x0001;
+const SECTION_MAP_WRITE: u32 = 0x0002;
+const SECTION_MAP_READ: u32 = 0x0004;
+const SECTION_MAP_EXECUTE: u32 = 0x0008;
+const SECTION_EXTEND_SIZE: u32 = 0x0010;
+const SECTION_ALL_ACCESS: u32 = STANDARD_RIGHTS_REQUIRED
+    | SECTION_QUERY
+    | SECTION_MAP_WRITE
+    | SECTION_MAP_READ
+    | SECTION_MAP_EXECUTE
+    | SECTION_EXTEND_SIZE;
+
+const FILE_MAP_ALL_ACCESS: u32 = SECTION_ALL_ACCESS;
+
+#[repr(C)]
+struct SECURITY_ATTRIBUTES {
+    length: u32,
+    descriptor: *mut c_void,
+    inherit: u32,
+}
+
+extern "system" {
+    /// <https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex>
+    fn LockFileEx(
+        file: RawHandle,
+        flags: u32,
+        _reserved: u32,
+        length_low: u32,
+        length_high: u32,
+        overlapped: *mut OVERLAPPED,
+    ) -> i32;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-unlockfileex>
+    fn UnlockFileEx(
+        file: RawHandle,
+        _reserved: u32,
+        length_low: u32,
+        length_high: u32,
+        overlapped: *mut OVERLAPPED,
+    ) -> i32;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-createfilemappingw>
+    fn CreateFileMappingW(
+        file: RawHandle,
+        attributes: *mut SECURITY_ATTRIBUTES,
+        protect: u32,
+        max_size_high: u32,
+        max_size_low: u32,
+        name: *const u16,
+    ) -> RawHandle;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffileex>
+    fn MapViewOfFileEx(
+        file_mapping: RawHandle,
+        desired_access: u32,
+        offset_high: u32,
+        offset_low: u32,
+        bytes_to_map: usize,
+        base_address: *mut u8,
+    ) -> *mut u8;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers>
+    fn FlushFileBuffers(file: RawHandle) -> u32;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-flushviewoffile>
+    fn FlushViewOfFile(base_address: *const u8, number_of_bytes_to_flush: usize) -> u32;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-unmapviewoffile>
+    fn UnmapViewOfFile(base_address: *const u8) -> u32;
+
+    /// <https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle>
+    fn CloseHandle(handle: RawHandle) -> u32;
+}
+
+struct AutoHandle {
+    inner: RawHandle,
+}
+
+impl Drop for AutoHandle {
+    fn drop(&mut self) {
+        unsafe {
+            CloseHandle(self.inner);
+        }
+    }
+}
+
+pub(super) struct FileLock {
+    handle: RawHandle,
+    overlapped: OVERLAPPED,
+}
+
+impl FileLock {
+    pub(super) fn new(file: &File) -> Result<Self> {
+        let handle = file.as_raw_handle();
+        let overlapped = unsafe {
+            let mut overlapped = std::mem::zeroed();
+            let result = LockFileEx(
+                handle,
+                LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
+                0,
+                u32::MAX,
+                u32::MAX,
+                &mut overlapped,
+            );
+
+            if result == 0 {
+                let err = io::Error::last_os_error();
+                return if err.raw_os_error() == Some(ERROR_IO_PENDING) {
+                    Err(Error::DatabaseAlreadyOpen)
+                } else {
+                    Err(Error::Io(err))
+                };
+            }
+
+            overlapped
+        };
+
+        Ok(Self { handle, overlapped })
+    }
+}
+
+impl Drop for FileLock {
+    fn drop(&mut self) {
+        unsafe { UnlockFileEx(self.handle, 0, u32::MAX, u32::MAX, &mut self.overlapped) };
+    }
+}
+
+pub(super) struct MmapInner {
+    pub(super) mmap: *mut u8,
+    pub(super) capacity: usize,
+}
+
+impl MmapInner {
+    pub(super) fn create_mapping(file: &File, len: u64, max_capacity: usize) -> Result<Self> {
+        // `CreateFileMappingW` documents:
+        //
+        // https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-createfilemappingw
+        // > An attempt to map a file with a length of 0 (zero) fails with an error code
+        // > of ERROR_FILE_INVALID. Applications should test for files with a length of 0
+        // > (zero) and reject those files.
+        assert!(len > 0);
+
+        let mmap = unsafe { Self::map_file(file, len, ptr::null_mut())? };
+
+        Ok(Self {
+            mmap,
+            capacity: max_capacity,
+        })
+    }
+
+    unsafe fn map_file(file: &File, len: u64, map_addr: *mut u8) -> Result<*mut u8> {
+        let handle = file.as_raw_handle();
+
+        let lo = (len & u32::MAX as u64) as u32;
+        let hi = (len >> 32) as u32;
+
+        let ptr = {
+            let mapping = AutoHandle {
+                inner: CreateFileMappingW(
+                    handle,
+                    ptr::null_mut(),
+                    PAGE_READWRITE,
+                    hi,
+                    lo,
+                    ptr::null(),
+                ),
+            };
+            if mapping.inner.is_null() {
+                return Err(Error::Io(io::Error::last_os_error()));
+            }
+
+            MapViewOfFileEx(
+                mapping.inner,
+                FILE_MAP_ALL_ACCESS,
+                0,
+                0,
+                len.try_into().unwrap(),
+                map_addr,
+            )
+        };
+
+        Ok(ptr)
+    }
+
+    pub(super) unsafe fn resize(&self, _len: u64, _owner: &Mmap) -> Result<()> {
+        unreachable!("Mmap resizing is not supported on Windows");
+        // UnmapViewOfFile(self.mmap);
+
+        // owner.file.set_len(len)?;
+        // let ptr = Self::map_file(&owner.file, len, self.mmap)?;
+
+        // // This is the same logic as in unix
+        // assert_eq!(ptr, self.mmap);
+
+        // Ok(())
+    }
+
+    pub(super) fn flush(&self, owner: &Mmap) -> Result {
+        self.eventual_flush(owner)?;
+
+        #[cfg(not(fuzzing))]
+        {
+            if unsafe { FlushFileBuffers(owner.file.as_raw_handle()) } == 0 {
+                return Err(Error::Io(io::Error::last_os_error()));
+            }
+        }
+        Ok(())
+    }
+
+    #[inline]
+    pub(super) fn eventual_flush(&self, owner: &Mmap) -> Result {
+        #[cfg(not(fuzzing))]
+        {
+            let result = unsafe { FlushViewOfFile(self.mmap, owner.len()) };
+            if result != 0 {
+                Ok(())
+            } else {
+                Err(Error::Io(io::Error::last_os_error()))
+            }
+        }
+
+        #[cfg(fuzzing)]
+        {
+            Ok(())
+        }
+    }
+}
+
+impl Drop for MmapInner {
+    fn drop(&mut self) {
+        unsafe {
+            UnmapViewOfFile(self.mmap);
+        }
+    }
+}
diff --git a/src/tree_store/page_store/page_manager.rs b/src/tree_store/page_store/page_manager.rs
index 30ff39b..11a0b12 100644
--- a/src/tree_store/page_store/page_manager.rs
+++ b/src/tree_store/page_store/page_manager.rs
@@ -760,43 +760,39 @@ impl TransactionalMemory {
             return Err(Error::OutOfSpace);
         }
 
-        let mmap = Mmap::new(file, max_capacity.try_into().unwrap())?;
-        if mmap.len() < DB_HEADER_SIZE {
-            // Safety: We're growing the mmap
-            unsafe {
-                mmap.resize(DB_HEADER_SIZE)?;
-            }
-        }
-
-        let mutex = Mutex::new(MetadataGuard {});
-        let mut metadata = unsafe { MetadataAccessor::new(&mmap, mutex.lock().unwrap()) };
-
         let region_size = requested_region_size
             .map(|x| x as u64)
             .unwrap_or(MAX_USABLE_REGION_SPACE);
         assert!(region_size.is_power_of_two());
         let max_usable_region_bytes = min(region_size, max_capacity.next_power_of_two() as u64);
 
-        if metadata.get_magic_number() != MAGICNUMBER {
-            let starting_size = if dynamic_growth {
-                MIN_DESIRED_USABLE_BYTES as u64
-            } else {
-                max_capacity
-            };
-            let layout = DatabaseLayout::calculate(
-                max_capacity,
-                starting_size,
-                (max_usable_region_bytes / page_size as u64) as u32,
-                page_size as u32,
-            )?;
-
-            if (mmap.len() as u64) < layout.len() {
-                // Safety: We're growing the mmap
-                unsafe {
-                    mmap.resize(layout.len().try_into().unwrap())?;
-                }
+        let starting_size = if dynamic_growth {
+            MIN_DESIRED_USABLE_BYTES as u64
+        } else {
+            max_capacity
+        };
+        let layout = DatabaseLayout::calculate(
+            max_capacity,
+            starting_size,
+            (max_usable_region_bytes / page_size as u64) as u32,
+            page_size as u32,
+        )?;
+
+        let max_cap: usize = max_capacity.try_into().unwrap();
+        {
+            let file_len = Mmap::get_valid_length(&file, max_cap)?;
+
+            if file_len < layout.len() {
+                file.set_len(layout.len())?;
             }
+        }
 
+        let mmap = Mmap::new(file, max_cap)?;
+
+        let mutex = Mutex::new(MetadataGuard {});
+        let mut metadata = unsafe { MetadataAccessor::new(&mmap, mutex.lock().unwrap()) };
+
+        if metadata.get_magic_number() != MAGICNUMBER {
             // Explicitly zero the header
             metadata.header.fill(0);
 
@@ -897,7 +893,7 @@ impl TransactionalMemory {
         let needs_recovery = metadata.get_recovery_required();
         drop(metadata);
 
-        Ok(TransactionalMemory {
+        Ok(Self {
             allocated_since_commit: Mutex::new(HashSet::new()),
             log_since_commit: Mutex::new(vec![]),
             needs_recovery,
diff --git a/src/tree_store/page_store/xxh3.rs b/src/tree_store/page_store/xxh3.rs
index 4314179..ca4644a 100644
--- a/src/tree_store/page_store/xxh3.rs
+++ b/src/tree_store/page_store/xxh3.rs
@@ -70,9 +70,7 @@ pub fn hash64_with_seed(data: &[u8], seed: u64) -> u64 {
         }
         #[cfg(target_arch = "aarch64")]
         {
-            unsafe {
-                return hash64_large_neon(data, seed);
-            }
+            unsafe { hash64_large_neon(data, seed) }
         }
         #[cfg(not(target_arch = "aarch64"))]
         hash64_large_generic(
@@ -99,9 +97,7 @@ pub fn hash128_with_seed(data: &[u8], seed: u64) -> u128 {
         }
         #[cfg(target_arch = "aarch64")]
         {
-            unsafe {
-                return hash128_large_neon(data, seed);
-            }
+            unsafe { hash128_large_neon(data, seed) }
         }
         #[cfg(not(target_arch = "aarch64"))]
         hash128_large_generic(


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 3b3b697..1a53f50 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -4,9 +4,11 @@ use tempfile::NamedTempFile;
 
 use rand::prelude::SliceRandom;
 use rand::Rng;
+#[cfg(unix)]
+use redb::ReadableMultimapTable;
 use redb::{
-    Builder, Database, Durability, Error, MultimapTableDefinition, ReadableMultimapTable,
-    ReadableTable, TableDefinition, WriteStrategy,
+    Builder, Database, Durability, Error, MultimapTableDefinition, ReadableTable, TableDefinition,
+    WriteStrategy,
 };
 
 const ELEMENTS: usize = 100;
@@ -143,11 +145,15 @@ fn free() {
     let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
 
     let db_size = 8 * 1024 * 1024;
+    #[cfg_attr(windows, allow(unused_mut))]
     let db = unsafe {
-        Database::builder()
-            .set_dynamic_growth(false)
-            .create(tmpfile.path(), db_size)
-            .unwrap()
+        let mut builder = Database::builder();
+        #[cfg(unix)]
+        {
+            builder.set_dynamic_growth(false);
+        }
+
+        builder.create(tmpfile.path(), db_size).unwrap()
     };
     let txn = db.begin_write().unwrap();
     {
@@ -271,6 +277,7 @@ fn large_keys() {
 }
 
 #[test]
+#[cfg(unix)]
 fn dynamic_growth() {
     let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
     let table_definition: TableDefinition<u64, [u8]> = TableDefinition::new("x");
@@ -303,6 +310,7 @@ fn dynamic_growth() {
 }
 
 #[test]
+#[cfg(unix)]
 fn dynamic_shrink() {
     let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
     let table_definition: TableDefinition<u64, [u8]> = TableDefinition::new("x");
@@ -771,6 +779,8 @@ fn regression13() {
 }
 
 #[test]
+// Disabling on Windows for now since this requires a minimum of 140GiB of disk space due to lack of resizing
+#[cfg(unix)]
 fn regression14() {
     let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
 
@@ -811,6 +821,8 @@ fn regression14() {
 }
 
 #[test]
+// Disabling on Windows for now since this requires a minimum of 400GiB of disk space due to lack of resizing
+#[cfg(unix)]
 fn regression15() {
     let tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
     let db_size = 400998358365;
diff --git a/tests/mmap_tests.rs b/tests/mmap_tests.rs
index 63128dd..21a5b73 100644
--- a/tests/mmap_tests.rs
+++ b/tests/mmap_tests.rs
@@ -1,11 +1,12 @@
-use std::os::unix::io::AsRawFd;
-use std::sync::mpsc::channel;
-use std::{ptr, slice, thread};
-use tempfile::NamedTempFile;
-
+#[cfg(unix)]
 #[test]
 // Test that reloading an mmap'ed region is thread-safe and atomic
 fn reload_mmap() {
+    use std::os::unix::io::AsRawFd;
+    use std::sync::mpsc::channel;
+    use std::{ptr, slice, thread};
+    use tempfile::NamedTempFile;
+
     struct SendPtr(*mut libc::c_void);
     unsafe impl Send for SendPtr {}