Skip to main content

uqa_storage/native_file/
shared_mapping.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Shared bytes of a local coordination file, accessed only under its external lock.
8
9use std::{fs::File, io, os::fd::AsRawFd, ptr::NonNull};
10
11mod allocation;
12
13/// A retained mapping, without a second file descriptor or borrowed Rust slices.
14/// Mapping ownership may move between threads; byte access requires external
15/// exclusion, extent validation and memory barriers at the lock boundary.
16pub struct SharedFileMapping {
17    address: NonNull<u8>,
18    length: usize,
19}
20
21// SAFETY: moving the mapping changes no address or ownership. Access is unsafe
22// and requires external exclusion; this type deliberately does not implement Sync.
23unsafe impl Send for SharedFileMapping {}
24
25impl SharedFileMapping {
26    /// Map an existing prefix, reserving sparse holes before writable access.
27    /// The caller must serialize this operation with file writes and resizing.
28    /// The existing file bytes and length are preserved, as is its seek position.
29    /// Unsupported filesystems or allocation failures should use positioned I/O.
30    pub fn new(file: &File, length: usize) -> io::Result<Self> {
31        let metadata = file.metadata()?;
32        if !metadata.is_file()
33            || length == 0
34            || length > isize::MAX as usize
35            || length as u64 > metadata.len()
36        {
37            return Err(io::Error::new(
38                io::ErrorKind::InvalidInput,
39                "invalid mapped file extent",
40            ));
41        }
42        require_local_filesystem(file)?;
43        allocation::reserve(file, length)?;
44        // SAFETY: mmap borrows an open read/write file, with a checked nonzero
45        // existing extent; it creates a new mapping without replacing any address.
46        let address = unsafe {
47            libc::mmap(
48                std::ptr::null_mut(),
49                length,
50                libc::PROT_READ | libc::PROT_WRITE,
51                libc::MAP_SHARED,
52                file.as_raw_fd(),
53                0,
54            )
55        };
56        if address == libc::MAP_FAILED {
57            return Err(io::Error::last_os_error());
58        }
59        let Some(address) = NonNull::new(address.cast::<u8>()) else {
60            // SAFETY: even a zero-address mapping must be released on rejection.
61            unsafe { libc::munmap(address, length) };
62            return Err(io::Error::other("mapped file has a null address"));
63        };
64        Ok(Self { address, length })
65    }
66
67    pub fn mapped_len(&self) -> usize {
68        self.length
69    }
70
71    pub fn contains(&self, offset: u64, length: usize) -> bool {
72        self.start(offset, length).is_some()
73    }
74
75    fn start(&self, offset: u64, length: usize) -> Option<usize> {
76        let start = usize::try_from(offset).ok()?;
77        (start.checked_add(length)? <= self.length).then_some(start)
78    }
79
80    /// Copy a mapped range, returning false if it is outside this mapping.
81    ///
82    /// # Safety
83    /// The underlying file must still cover the mapped extent. Exclude writes
84    /// and resizing in every process for the complete copy, with an acquire
85    /// barrier after taking the external lock. The destination must not alias
86    /// the mapping. No reference into the mapped memory escapes this call.
87    pub unsafe fn read(&self, offset: u64, bytes: &mut [u8]) -> bool {
88        let Some(start) = self.start(offset, bytes.len()) else {
89            return false;
90        };
91        // SAFETY: the caller maintains the live extent and exclusion; start
92        // bounds the complete copy, whose destination is separately owned.
93        unsafe {
94            std::ptr::copy_nonoverlapping(
95                self.address.as_ptr().add(start),
96                bytes.as_mut_ptr(),
97                bytes.len(),
98            );
99        }
100        true
101    }
102
103    /// Replace a mapped range, returning false if it is outside this mapping.
104    ///
105    /// # Safety
106    /// The underlying file must still cover the reserved mapped extent. Exclude
107    /// all other access and resizing until the copy completes, then issue a
108    /// release barrier before unlocking. The source must not alias the mapping.
109    /// This publishes coordination bytes; it does not provide crash durability.
110    pub unsafe fn write(&self, offset: u64, bytes: &[u8]) -> bool {
111        let Some(start) = self.start(offset, bytes.len()) else {
112            return false;
113        };
114        // SAFETY: the caller maintains the reserved extent and exclusive access;
115        // start bounds the complete copy from a separately owned source.
116        unsafe {
117            std::ptr::copy_nonoverlapping(
118                bytes.as_ptr(),
119                self.address.as_ptr().add(start),
120                bytes.len(),
121            );
122        }
123        true
124    }
125
126    /// Replace a record while publishing its state byte last. An interrupted
127    /// copy leaves `pending`, so a peer never decodes a partially written record.
128    /// Returns false without mutation when either range is invalid.
129    ///
130    /// # Safety
131    /// The exclusion, extent and source requirements of `write` apply. `pending`
132    /// must describe a valid unpublished record in the caller's format, regardless
133    /// of its remaining bytes. Its state byte must be independently writable.
134    pub unsafe fn write_published(
135        &self,
136        offset: u64,
137        bytes: &[u8],
138        state: usize,
139        pending: u8,
140    ) -> bool {
141        use std::sync::atomic::{AtomicU8, Ordering};
142        let Some(start) = self
143            .start(offset, bytes.len())
144            .filter(|_| state < bytes.len())
145        else {
146            return false;
147        };
148        // SAFETY: the validated state byte is aligned for AtomicU8. Exclusion
149        // prevents concurrent atomic/non-atomic access to the mapped bytes.
150        let publication = unsafe { AtomicU8::from_ptr(self.address.as_ptr().add(start + state)) };
151        // SeqCst also orders the following payload copies after invalidation.
152        publication.store(pending, Ordering::SeqCst);
153        unsafe {
154            self.write(offset, &bytes[..state]);
155            self.write(offset + state as u64 + 1, &bytes[state + 1..]);
156        }
157        #[cfg(test)]
158        assert!(
159            !INTERRUPT_PUBLICATION.replace(false),
160            "interrupted record publication"
161        );
162        publication.store(bytes[state], Ordering::Release);
163        true
164    }
165}
166
167#[cfg(test)]
168thread_local! {
169    static INTERRUPT_PUBLICATION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
170}
171
172impl Drop for SharedFileMapping {
173    fn drop(&mut self) {
174        // SAFETY: this object uniquely owns this mapping and no borrowed slice
175        // exists. Unmapping neither opens nor closes a coordination descriptor.
176        unsafe { libc::munmap(self.address.as_ptr().cast(), self.length) };
177    }
178}
179
180fn require_local_filesystem(file: &File) -> io::Result<()> {
181    let mut filesystem = std::mem::MaybeUninit::<libc::statfs>::uninit();
182    // SAFETY: the descriptor and correctly sized output are live for fstatfs.
183    if unsafe { libc::fstatfs(file.as_raw_fd(), filesystem.as_mut_ptr()) } != 0 {
184        return Err(io::Error::last_os_error());
185    }
186    let filesystem = unsafe { filesystem.assume_init() };
187    #[cfg(target_os = "linux")]
188    let supported = matches!(
189        filesystem.f_type as u64,
190        0xef53 | 0x5846_5342 | 0x9123_683e | 0x0102_1994 | 0x8584_58f6
191    );
192    #[cfg(target_os = "macos")]
193    let supported = {
194        let name = unsafe { std::ffi::CStr::from_ptr(filesystem.f_fstypename.as_ptr()) };
195        filesystem.f_flags & libc::MNT_LOCAL as u32 != 0
196            && matches!(name.to_bytes(), b"apfs" | b"hfs")
197    };
198    if supported {
199        Ok(())
200    } else {
201        Err(io::Error::new(
202            io::ErrorKind::Unsupported,
203            "file mapping requires a supported local filesystem",
204        ))
205    }
206}
207
208#[cfg(test)]
209mod tests;