use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::fs::{self, File, OpenOptions};
use std::os::windows::fs::{FileExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering, fence};
use std::sync::{Arc, Mutex, OnceLock};
use advisory_lock::{AdvisoryFileLock, FileLockError, FileLockMode};
use fsqlite_error::{FrankenError, Result};
use fsqlite_types::LockLevel;
use fsqlite_types::cx::Cx;
use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
use tracing::{debug, info};
use crate::shm::{
SQLITE_SHM_EXCLUSIVE, SQLITE_SHM_LOCK, SQLITE_SHM_SHARED, SQLITE_SHM_UNLOCK, ShmRegion,
WAL_TOTAL_LOCKS,
};
use crate::traits::{Vfs, VfsFile};
const SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN: u32 = 0x0000_0800;
const WINDOWS_FILE_SHARE_READ: u32 = 0x0000_0001;
const WINDOWS_FILE_SHARE_WRITE: u32 = 0x0000_0002;
const WINDOWS_SHARE_READ_WRITE: u32 = WINDOWS_FILE_SHARE_READ | WINDOWS_FILE_SHARE_WRITE;
fn checkpoint_or_abort(cx: &Cx) -> Result<()> {
cx.checkpoint().map_err(|_| FrankenError::Abort)
}
fn lock_poisoned(name: &str) -> FrankenError {
FrankenError::internal(format!("{name} lock poisoned"))
}
fn windows_open_options() -> OpenOptions {
let mut options = OpenOptions::new();
options.share_mode(WINDOWS_SHARE_READ_WRITE);
options
}
fn resolve_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(env::current_dir()?.join(path))
}
}
fn sqlite_shm_path(path: &Path) -> PathBuf {
let mut shm: OsString = path.as_os_str().to_owned();
shm.push("-shm");
PathBuf::from(shm)
}
fn sqlite_shared_lock_path(path: &Path) -> PathBuf {
let mut p: OsString = path.as_os_str().to_owned();
p.push("-lock-shared");
PathBuf::from(p)
}
fn sqlite_reserved_lock_path(path: &Path) -> PathBuf {
let mut p: OsString = path.as_os_str().to_owned();
p.push("-lock-reserved");
PathBuf::from(p)
}
fn sqlite_pending_lock_path(path: &Path) -> PathBuf {
let mut p: OsString = path.as_os_str().to_owned();
p.push("-lock-pending");
PathBuf::from(p)
}
fn windows_lock_sidecar_paths(path: &Path) -> [PathBuf; 3] {
[
sqlite_shared_lock_path(path),
sqlite_reserved_lock_path(path),
sqlite_pending_lock_path(path),
]
}
fn try_remove_windows_lock_sidecars(path: &Path) {
for sidecar in windows_lock_sidecar_paths(path) {
let _ = fs::remove_file(sidecar);
}
}
fn ensure_shm_file_len(path: &Path, min_len: u64) -> Result<()> {
let mut options = windows_open_options();
let file = options
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
let current = file.metadata()?.len();
if current < min_len {
file.set_len(min_len)?;
}
Ok(())
}
fn open_windows_lock_sidecar(path: &Path) -> Result<(File, bool)> {
loop {
let mut create_options = windows_open_options();
match create_options
.read(true)
.write(true)
.create_new(true)
.open(path)
{
Ok(file) => return Ok((file, true)),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
let mut open_options = windows_open_options();
match open_options.read(true).write(true).open(path) {
Ok(file) => return Ok((file, false)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(FrankenError::Io(err)),
}
}
Err(err) => return Err(FrankenError::Io(err)),
}
}
}
#[derive(Debug, Default)]
struct WindowsVfsInner {
next_temp_id: u64,
}
#[derive(Debug, Clone, Default)]
pub struct WindowsVfs {
inner: Arc<Mutex<WindowsVfsInner>>,
}
impl WindowsVfs {
#[must_use]
pub fn new() -> Self {
info!(
target: "fsqlite_vfs::windows",
sector_size = 4096_u32,
"windows vfs initialized"
);
Self::default()
}
fn next_temp_path(&self) -> Result<PathBuf> {
let mut inner = self
.inner
.lock()
.map_err(|_| lock_poisoned("windows vfs inner"))?;
let id = inner.next_temp_id.max(next_temp_id());
inner.next_temp_id = id
.checked_add(1)
.ok_or_else(|| FrankenError::internal("temp file id overflow"))?;
Ok(env::temp_dir().join(format!("fsqlite-windows-{id}.tmp")))
}
}
#[derive(Debug, Clone, Default)]
struct ShmSlotState {
shared_holders: HashMap<u64, u32>,
exclusive_owner: Option<u64>,
}
#[derive(Debug)]
struct WindowsShmState {
regions: HashMap<u32, ShmRegion>,
slots: Vec<ShmSlotState>,
owner_refs: HashMap<u64, u32>,
}
impl Default for WindowsShmState {
fn default() -> Self {
let slot_count = usize::try_from(WAL_TOTAL_LOCKS).expect("WAL_TOTAL_LOCKS must fit usize");
Self {
regions: HashMap::new(),
slots: vec![ShmSlotState::default(); slot_count],
owner_refs: HashMap::new(),
}
}
}
#[derive(Debug)]
struct WindowsShmTable {
map: Mutex<HashMap<PathBuf, Arc<Mutex<WindowsShmState>>>>,
}
impl WindowsShmTable {
fn new() -> Self {
Self {
map: Mutex::new(HashMap::new()),
}
}
fn get(&self, path: &Path) -> Result<Option<Arc<Mutex<WindowsShmState>>>> {
let map = self
.map
.lock()
.map_err(|_| lock_poisoned("windows shm table"))?;
Ok(map.get(path).map(Arc::clone))
}
fn get_or_create(&self, path: &Path) -> Result<Arc<Mutex<WindowsShmState>>> {
let mut map = self
.map
.lock()
.map_err(|_| lock_poisoned("windows shm table"))?;
Ok(Arc::clone(map.entry(path.to_path_buf()).or_insert_with(
|| Arc::new(Mutex::new(WindowsShmState::default())),
)))
}
fn remove_if_orphaned(&self, path: &Path) -> Result<()> {
let mut map = self
.map
.lock()
.map_err(|_| lock_poisoned("windows shm table"))?;
if let Some(state) = map.get(path) {
let orphaned = state
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?
.owner_refs
.is_empty();
if orphaned {
map.remove(path);
}
}
Ok(())
}
}
fn windows_shm_table() -> &'static WindowsShmTable {
static TABLE: OnceLock<WindowsShmTable> = OnceLock::new();
TABLE.get_or_init(WindowsShmTable::new)
}
fn next_owner_id() -> u64 {
static OWNER_SEQ: AtomicU64 = AtomicU64::new(1);
OWNER_SEQ.fetch_add(1, Ordering::Relaxed)
}
fn next_temp_id() -> u64 {
static TEMP_SEQ: AtomicU64 = AtomicU64::new(1);
TEMP_SEQ.fetch_add(1, Ordering::Relaxed)
}
fn to_slot_index(slot: u32) -> Result<usize> {
usize::try_from(slot).map_err(|_| FrankenError::OutOfRange {
what: "shm slot index".to_string(),
value: slot.to_string(),
})
}
fn next_lock_level(level: LockLevel) -> Option<LockLevel> {
match level {
LockLevel::None => Some(LockLevel::Shared),
LockLevel::Shared => Some(LockLevel::Reserved),
LockLevel::Reserved => Some(LockLevel::Pending),
LockLevel::Pending => Some(LockLevel::Exclusive),
LockLevel::Exclusive => None,
}
}
fn lock_level_slot(level: LockLevel) -> Option<usize> {
match level {
LockLevel::None => None,
LockLevel::Shared => Some(0),
LockLevel::Reserved => Some(1),
LockLevel::Pending => Some(2),
LockLevel::Exclusive => Some(3),
}
}
#[derive(Debug)]
struct WindowsOsLockFiles {
shared_file: File,
reserved_file: File,
pending_file: File,
held_levels: [bool; 4],
}
impl WindowsOsLockFiles {
fn open(path: &Path) -> Result<Self> {
let shared_path = sqlite_shared_lock_path(path);
let reserved_path = sqlite_reserved_lock_path(path);
let pending_path = sqlite_pending_lock_path(path);
let (shared_file, shared_created) = open_windows_lock_sidecar(&shared_path)?;
let (reserved_file, reserved_created) = match open_windows_lock_sidecar(&reserved_path) {
Ok(opened) => opened,
Err(err) => {
drop(shared_file);
if shared_created {
let _ = fs::remove_file(&shared_path);
}
return Err(err);
}
};
let (pending_file, _) = match open_windows_lock_sidecar(&pending_path) {
Ok(opened) => opened,
Err(err) => {
drop(reserved_file);
drop(shared_file);
if reserved_created {
let _ = fs::remove_file(&reserved_path);
}
if shared_created {
let _ = fs::remove_file(&shared_path);
}
return Err(err);
}
};
Ok(Self {
shared_file,
reserved_file,
pending_file,
held_levels: [false; 4],
})
}
fn try_lock_shared(file: &File) -> Result<()> {
match AdvisoryFileLock::try_lock(file, FileLockMode::Shared) {
Ok(()) => Ok(()),
Err(FileLockError::AlreadyLocked) => Err(FrankenError::Busy),
Err(FileLockError::Io(err)) => Err(FrankenError::Io(err)),
}
}
fn try_lock_exclusive(file: &File) -> Result<()> {
match AdvisoryFileLock::try_lock(file, FileLockMode::Exclusive) {
Ok(()) => Ok(()),
Err(FileLockError::AlreadyLocked) => Err(FrankenError::Busy),
Err(FileLockError::Io(err)) => Err(FrankenError::Io(err)),
}
}
fn unlock_file(file: &File) -> Result<()> {
match AdvisoryFileLock::unlock(file) {
Ok(()) => Ok(()),
Err(FileLockError::AlreadyLocked) => Err(FrankenError::LockFailed {
detail: "unlock called for contended lock".to_string(),
}),
Err(FileLockError::Io(err)) => Err(FrankenError::Io(err)),
}
}
fn lock_file_for_level(&self, level: LockLevel) -> Option<&File> {
match level {
LockLevel::None => None,
LockLevel::Shared | LockLevel::Exclusive => Some(&self.shared_file),
LockLevel::Reserved => Some(&self.reserved_file),
LockLevel::Pending => Some(&self.pending_file),
}
}
fn lock_held(&self, level: LockLevel) -> bool {
lock_level_slot(level).is_some_and(|slot| self.held_levels[slot])
}
fn set_lock_held(&mut self, level: LockLevel, held: bool) {
if let Some(slot) = lock_level_slot(level) {
self.held_levels[slot] = held;
}
}
fn try_lock_level(&mut self, level: LockLevel) -> Result<()> {
if level == LockLevel::None {
return Ok(());
}
if self.lock_held(level) {
return Ok(());
}
if level == LockLevel::Shared {
Self::try_lock_shared(&self.pending_file)?;
let shared_result = Self::try_lock_shared(&self.shared_file);
let pending_unlock = Self::unlock_file(&self.pending_file);
if let Err(err) = shared_result {
pending_unlock?;
return Err(err);
}
if let Err(err) = pending_unlock {
let _ = Self::unlock_file(&self.shared_file);
return Err(err);
}
self.set_lock_held(LockLevel::Shared, true);
return Ok(());
}
if level == LockLevel::Exclusive {
let had_shared = self.lock_held(LockLevel::Shared);
if had_shared {
Self::unlock_file(&self.shared_file)?;
self.set_lock_held(LockLevel::Shared, false);
}
if let Err(err) = Self::try_lock_exclusive(&self.shared_file) {
if had_shared && Self::try_lock_shared(&self.shared_file).is_ok() {
self.set_lock_held(LockLevel::Shared, true);
}
return Err(err);
}
self.set_lock_held(LockLevel::Exclusive, true);
return Ok(());
}
let file = self
.lock_file_for_level(level)
.ok_or_else(|| FrankenError::internal("invalid lock level"))?;
Self::try_lock_exclusive(file)?;
self.set_lock_held(level, true);
Ok(())
}
fn unlock_to(&mut self, level: LockLevel) -> Result<()> {
if self.lock_held(LockLevel::Exclusive) && level < LockLevel::Exclusive {
Self::unlock_file(&self.shared_file)?;
self.set_lock_held(LockLevel::Exclusive, false);
if level >= LockLevel::Shared {
Self::try_lock_shared(&self.shared_file)?;
self.set_lock_held(LockLevel::Shared, true);
}
}
for held_level in [LockLevel::Pending, LockLevel::Reserved, LockLevel::Shared] {
if level < held_level && self.lock_held(held_level) {
let file = self
.lock_file_for_level(held_level)
.ok_or_else(|| FrankenError::internal("invalid lock level"))?;
Self::unlock_file(file)?;
self.set_lock_held(held_level, false);
}
}
Ok(())
}
fn highest_held_level(&self) -> LockLevel {
[
LockLevel::Exclusive,
LockLevel::Pending,
LockLevel::Reserved,
LockLevel::Shared,
]
.into_iter()
.find(|level| self.lock_held(*level))
.unwrap_or(LockLevel::None)
}
fn reserved_locked_by_other(&self) -> Result<bool> {
if self.lock_held(LockLevel::Reserved) {
return Ok(false);
}
match AdvisoryFileLock::try_lock(&self.reserved_file, FileLockMode::Exclusive) {
Ok(()) => {
Self::unlock_file(&self.reserved_file)?;
Ok(false)
}
Err(FileLockError::AlreadyLocked) => Ok(true),
Err(FileLockError::Io(err)) => Err(FrankenError::Io(err)),
}
}
}
impl Vfs for WindowsVfs {
type File = WindowsFile;
fn name(&self) -> &'static str {
"windows"
}
#[allow(clippy::significant_drop_tightening)]
fn open(
&self,
cx: &Cx,
path: Option<&Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
checkpoint_or_abort(cx)?;
let is_temp = path.is_none();
let mut resolved = if let Some(path) = path {
resolve_path(path)?
} else {
self.next_temp_path()?
};
let is_create = path.is_none() || flags.contains(VfsOpenFlags::CREATE);
let is_rw = path.is_none() || flags.contains(VfsOpenFlags::READWRITE) || is_create;
let is_exclusive_create = is_create && flags.contains(VfsOpenFlags::EXCLUSIVE);
if !is_create && !resolved.exists() {
return Err(FrankenError::CannotOpen { path: resolved });
}
let mut created_db_file = false;
let file = loop {
let mut options = windows_open_options();
options.read(true);
if is_rw {
options.write(true);
}
if is_create {
options.create_new(true);
}
match options.open(&resolved) {
Ok(file) => {
created_db_file = is_create;
break file;
}
Err(err) if is_temp && err.kind() == std::io::ErrorKind::AlreadyExists => {
resolved = self.next_temp_path()?;
}
Err(err)
if is_create
&& !is_temp
&& !is_exclusive_create
&& err.kind() == std::io::ErrorKind::AlreadyExists =>
{
let mut open_options = windows_open_options();
open_options.read(true);
if is_rw {
open_options.write(true);
}
match open_options.open(&resolved) {
Ok(file) => break file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(FrankenError::Io(err)),
}
}
Err(err) => {
return Err(if err.kind() == std::io::ErrorKind::NotFound {
FrankenError::CannotOpen { path: resolved }
} else {
FrankenError::Io(err)
});
}
}
};
let owner_id = next_owner_id();
let shm_path = sqlite_shm_path(&resolved);
let delete_on_close = flags.contains(VfsOpenFlags::DELETEONCLOSE) || is_temp;
let out_flags = if is_create {
flags | VfsOpenFlags::READWRITE
} else {
flags
};
let os_locks = match WindowsOsLockFiles::open(&resolved) {
Ok(os_locks) => os_locks,
Err(err) => {
drop(file);
if created_db_file {
let _ = fs::remove_file(&resolved);
}
return Err(err);
}
};
Ok((
WindowsFile {
path: resolved,
file: Some(file),
os_locks: Some(os_locks),
owner_id,
lock_level: LockLevel::None,
delete_on_close,
shm_path,
shm_state: None,
},
out_flags,
))
}
fn delete(&self, _cx: &Cx, path: &Path, _sync_dir: bool) -> Result<()> {
let resolved = resolve_path(path)?;
if resolved.exists() {
fs::remove_file(&resolved)?;
}
let shm_path = sqlite_shm_path(&resolved);
if shm_path.exists() {
fs::remove_file(shm_path)?;
}
try_remove_windows_lock_sidecars(&resolved);
Ok(())
}
fn access(&self, _cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool> {
let resolved = resolve_path(path)?;
if !resolved.exists() {
return Ok(false);
}
match flags {
f if f == AccessFlags::EXISTS => Ok(true),
f if f == AccessFlags::READ => {
let mut options = windows_open_options();
Ok(options.read(true).open(resolved).is_ok())
}
_ => {
let mut options = windows_open_options();
Ok(options.read(true).write(true).open(resolved).is_ok())
}
}
}
fn full_pathname(&self, _cx: &Cx, path: &Path) -> Result<PathBuf> {
resolve_path(path)
}
}
#[derive(Debug)]
pub struct WindowsFile {
path: PathBuf,
file: Option<File>,
os_locks: Option<WindowsOsLockFiles>,
owner_id: u64,
lock_level: LockLevel,
delete_on_close: bool,
shm_path: PathBuf,
shm_state: Option<Arc<Mutex<WindowsShmState>>>,
}
impl WindowsFile {
fn is_closed(&self) -> bool {
self.file.is_none() && self.os_locks.is_none()
}
fn ensure_open(&self) -> Result<()> {
if self.is_closed() {
Err(FrankenError::internal("windows file is closed"))
} else {
Ok(())
}
}
fn file_ref(&self) -> Result<&File> {
self.file
.as_ref()
.ok_or_else(|| FrankenError::internal("windows file is closed"))
}
fn file_mut(&mut self) -> Result<&mut File> {
self.file
.as_mut()
.ok_or_else(|| FrankenError::internal("windows file is closed"))
}
fn os_locks_ref(&self) -> Result<&WindowsOsLockFiles> {
self.os_locks
.as_ref()
.ok_or_else(|| FrankenError::internal("windows lock files are closed"))
}
fn os_locks_mut(&mut self) -> Result<&mut WindowsOsLockFiles> {
self.os_locks
.as_mut()
.ok_or_else(|| FrankenError::internal("windows lock files are closed"))
}
fn ensure_shm_state(&mut self) -> Result<Arc<Mutex<WindowsShmState>>> {
if let Some(state) = &self.shm_state {
return Ok(Arc::clone(state));
}
let state = windows_shm_table().get_or_create(&self.shm_path)?;
{
let mut guard = state
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?;
*guard.owner_refs.entry(self.owner_id).or_insert(0) += 1;
}
self.shm_state = Some(Arc::clone(&state));
Ok(state)
}
fn release_shm_owner_state(&mut self, delete: bool) -> Result<()> {
let Some(state_arc) = self.shm_state.take() else {
if delete {
drop(fs::remove_file(&self.shm_path));
}
return Ok(());
};
let orphaned = {
let mut state = state_arc
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?;
for slot in &mut state.slots {
slot.shared_holders.remove(&self.owner_id);
if slot.exclusive_owner == Some(self.owner_id) {
slot.exclusive_owner = None;
}
}
if let Some(count) = state.owner_refs.get_mut(&self.owner_id) {
if *count > 1 {
*count -= 1;
} else {
state.owner_refs.remove(&self.owner_id);
}
}
state.owner_refs.is_empty()
};
if orphaned {
windows_shm_table().remove_if_orphaned(&self.shm_path)?;
}
if delete {
drop(fs::remove_file(&self.shm_path));
}
Ok(())
}
fn validate_shm_request(offset: u32, n: u32) -> Result<u32> {
if n == 0 {
return Err(FrankenError::LockFailed {
detail: "shm_lock called with n=0".to_string(),
});
}
let end = offset
.checked_add(n)
.ok_or_else(|| FrankenError::LockFailed {
detail: "shm_lock range overflow".to_string(),
})?;
if end > WAL_TOTAL_LOCKS {
return Err(FrankenError::LockFailed {
detail: format!("shm_lock range {offset}..{end} exceeds WAL lock table"),
});
}
Ok(end)
}
fn acquire_shared_slot(state: &mut WindowsShmState, slot: u32, owner_id: u64) -> Result<()> {
let idx = to_slot_index(slot)?;
let slot_state = state
.slots
.get_mut(idx)
.ok_or_else(|| FrankenError::internal("shm slot index out of bounds"))?;
if let Some(exclusive_owner) = slot_state.exclusive_owner {
if exclusive_owner != owner_id {
return Err(FrankenError::Busy);
}
}
*slot_state.shared_holders.entry(owner_id).or_insert(0) += 1;
Ok(())
}
fn acquire_exclusive_slot(state: &mut WindowsShmState, slot: u32, owner_id: u64) -> Result<()> {
let idx = to_slot_index(slot)?;
let slot_state = state
.slots
.get_mut(idx)
.ok_or_else(|| FrankenError::internal("shm slot index out of bounds"))?;
if slot_state.exclusive_owner == Some(owner_id) {
return Ok(());
}
if slot_state.exclusive_owner.is_some() {
return Err(FrankenError::Busy);
}
if slot_state
.shared_holders
.iter()
.any(|(owner, count)| *owner != owner_id && *count > 0)
{
return Err(FrankenError::Busy);
}
slot_state.exclusive_owner = Some(owner_id);
Ok(())
}
fn release_shared_slot(state: &mut WindowsShmState, slot: u32, owner_id: u64) -> Result<()> {
let idx = to_slot_index(slot)?;
let slot_state = state
.slots
.get_mut(idx)
.ok_or_else(|| FrankenError::internal("shm slot index out of bounds"))?;
let Some(holder_count) = slot_state.shared_holders.get_mut(&owner_id) else {
return Err(FrankenError::LockFailed {
detail: format!("owner {owner_id} does not hold shared SHM slot {slot}"),
});
};
if *holder_count > 1 {
*holder_count -= 1;
} else {
slot_state.shared_holders.remove(&owner_id);
}
Ok(())
}
fn release_exclusive_slot(state: &mut WindowsShmState, slot: u32, owner_id: u64) -> Result<()> {
let idx = to_slot_index(slot)?;
let slot_state = state
.slots
.get_mut(idx)
.ok_or_else(|| FrankenError::internal("shm slot index out of bounds"))?;
if slot_state.exclusive_owner != Some(owner_id) {
return Err(FrankenError::LockFailed {
detail: format!("owner {owner_id} does not hold exclusive SHM slot {slot}"),
});
}
slot_state.exclusive_owner = None;
Ok(())
}
}
impl VfsFile for WindowsFile {
fn close(&mut self, cx: &Cx) -> Result<()> {
if self.is_closed() && self.shm_state.is_none() {
return Ok(());
}
let mut first_error = None;
if !self.is_closed() {
if let Err(err) = self.unlock(cx, LockLevel::None) {
first_error = Some(err);
}
}
let release_result = if self.shm_state.is_some() || self.delete_on_close {
self.release_shm_owner_state(self.delete_on_close)
} else {
Ok(())
};
if first_error.is_none() {
first_error = release_result.err();
}
drop(self.os_locks.take());
drop(self.file.take());
self.lock_level = LockLevel::None;
if self.delete_on_close {
drop(fs::remove_file(&self.path));
try_remove_windows_lock_sidecars(&self.path);
}
first_error.map_or(Ok(()), Err)
}
fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize> {
checkpoint_or_abort(cx)?;
let mut total = 0_usize;
while total < buf.len() {
let read_offset = offset
.checked_add(u64::try_from(total).map_err(|_| FrankenError::OutOfRange {
what: "read offset".to_string(),
value: total.to_string(),
})?)
.ok_or_else(|| FrankenError::OutOfRange {
what: "read offset".to_string(),
value: "overflow".to_string(),
})?;
let n = self.file_ref()?.seek_read(&mut buf[total..], read_offset)?;
if n == 0 {
break;
}
total += n;
}
if total < buf.len() {
buf[total..].fill(0);
}
Ok(total)
}
fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()> {
checkpoint_or_abort(cx)?;
let mut total = 0_usize;
while total < buf.len() {
let write_offset = offset
.checked_add(u64::try_from(total).map_err(|_| FrankenError::OutOfRange {
what: "write offset".to_string(),
value: total.to_string(),
})?)
.ok_or_else(|| FrankenError::OutOfRange {
what: "write offset".to_string(),
value: "overflow".to_string(),
})?;
let n = self.file_mut()?.seek_write(&buf[total..], write_offset)?;
if n == 0 {
return Err(FrankenError::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"seek_write returned 0",
)));
}
total += n;
}
Ok(())
}
fn truncate(&mut self, _cx: &Cx, size: u64) -> Result<()> {
self.file_mut()?.set_len(size)?;
Ok(())
}
fn sync(&mut self, _cx: &Cx, flags: SyncFlags) -> Result<()> {
if flags.contains(SyncFlags::DATAONLY) {
self.file_mut()?.sync_data()?;
} else {
self.file_mut()?.sync_all()?;
}
Ok(())
}
fn file_size(&self, _cx: &Cx) -> Result<u64> {
Ok(self.file_ref()?.metadata()?.len())
}
fn lock(&mut self, _cx: &Cx, level: LockLevel) -> Result<()> {
if level <= self.lock_level {
return Ok(());
}
let prior_level = self.lock_level;
while self.lock_level < level {
let next = next_lock_level(self.lock_level)
.ok_or_else(|| FrankenError::internal("invalid lock escalation"))?;
let lock_result = self.os_locks_mut()?.try_lock_level(next);
if let Err(err) = lock_result {
let highest_held_level = {
let os_locks = self.os_locks_mut()?;
let _ = os_locks.unlock_to(prior_level);
os_locks.highest_held_level()
};
self.lock_level = highest_held_level;
return Err(err);
}
self.lock_level = next;
}
Ok(())
}
fn unlock(&mut self, _cx: &Cx, level: LockLevel) -> Result<()> {
if level >= self.lock_level {
return Ok(());
}
let unlock_result = self.os_locks_mut()?.unlock_to(level);
if let Err(err) = unlock_result {
self.lock_level = self.os_locks_mut()?.highest_held_level();
return Err(err);
}
self.lock_level = level;
Ok(())
}
fn check_reserved_lock(&self, _cx: &Cx) -> Result<bool> {
self.os_locks_ref()?.reserved_locked_by_other()
}
fn sector_size(&self) -> u32 {
4096
}
fn device_characteristics(&self) -> u32 {
SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
}
#[allow(clippy::significant_drop_tightening)]
fn shm_map(&mut self, _cx: &Cx, region: u32, size: u32, extend: bool) -> Result<ShmRegion> {
self.ensure_open()?;
if size == 0 {
return Err(FrankenError::LockFailed {
detail: "shm_map size must be > 0".to_string(),
});
}
let size_usize = usize::try_from(size).map_err(|_| FrankenError::OutOfRange {
what: "shm region size".to_string(),
value: size.to_string(),
})?;
let min_len = u64::from(region)
.checked_add(1)
.and_then(|value| value.checked_mul(u64::from(size)))
.ok_or_else(|| FrankenError::OutOfRange {
what: "shm file length".to_string(),
value: format!("region={region}, size={size}"),
})?;
if !extend {
let mut needs_owner_ref = false;
let shm_state = if let Some(state) = &self.shm_state {
Arc::clone(state)
} else {
needs_owner_ref = true;
windows_shm_table().get(&self.shm_path)?.ok_or_else(|| {
FrankenError::CannotOpen {
path: self.shm_path.clone(),
}
})?
};
let mapped_region = {
let mut state = shm_state
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?;
let existing = state.regions.get(®ion).cloned().ok_or_else(|| {
FrankenError::CannotOpen {
path: self.shm_path.clone(),
}
})?;
if existing.len() < size_usize {
return Err(FrankenError::LockFailed {
detail: format!(
"shm region {region} is {} bytes, requested {size_usize} bytes without extend",
existing.len()
),
});
}
if needs_owner_ref {
*state.owner_refs.entry(self.owner_id).or_insert(0) += 1;
}
existing
};
if needs_owner_ref {
self.shm_state = Some(Arc::clone(&shm_state));
}
debug!(
target: "fsqlite_vfs::windows",
region,
size,
path = %self.shm_path.display(),
"mapped windows shm region"
);
return Ok(mapped_region);
}
ensure_shm_file_len(&self.shm_path, min_len)?;
let shm_state = self.ensure_shm_state()?;
let mapped_region = {
let mut state = shm_state
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?;
let entry = state.regions.entry(region);
let region_ref = match entry {
std::collections::hash_map::Entry::Occupied(occupied) => {
let region_ref = occupied.into_mut();
if region_ref.len() < size_usize {
region_ref.try_resize_heap(size_usize)?;
}
region_ref
}
std::collections::hash_map::Entry::Vacant(vacant) => {
vacant.insert(ShmRegion::new(size_usize))
}
};
region_ref.clone()
};
debug!(
target: "fsqlite_vfs::windows",
region,
size,
path = %self.shm_path.display(),
"mapped windows shm region"
);
Ok(mapped_region)
}
fn shm_lock(&mut self, _cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
self.ensure_open()?;
let end = Self::validate_shm_request(offset, n)?;
let lock_requested = flags & SQLITE_SHM_LOCK != 0;
let unlock_requested = flags & SQLITE_SHM_UNLOCK != 0;
if lock_requested == unlock_requested {
return Err(FrankenError::LockFailed {
detail: "invalid shm_lock flags (must set exactly one of LOCK/UNLOCK)".to_string(),
});
}
let shared_requested = flags & SQLITE_SHM_SHARED != 0;
let exclusive_requested = flags & SQLITE_SHM_EXCLUSIVE != 0;
if shared_requested == exclusive_requested {
return Err(FrankenError::LockFailed {
detail: "invalid shm_lock flags (must set exactly one of SHARED/EXCLUSIVE)"
.to_string(),
});
}
let shm_state = self.ensure_shm_state()?;
let mut state = shm_state
.lock()
.map_err(|_| lock_poisoned("windows shm state"))?;
if lock_requested {
let mut acquired: Vec<u32> = Vec::new();
for slot in offset..end {
let result = if exclusive_requested {
Self::acquire_exclusive_slot(&mut state, slot, self.owner_id)
} else {
Self::acquire_shared_slot(&mut state, slot, self.owner_id)
};
if let Err(err) = result {
for acquired_slot in acquired.into_iter().rev() {
let rollback = if exclusive_requested {
Self::release_exclusive_slot(&mut state, acquired_slot, self.owner_id)
} else {
Self::release_shared_slot(&mut state, acquired_slot, self.owner_id)
};
if rollback.is_err() {
break;
}
}
return Err(err);
}
acquired.push(slot);
}
return Ok(());
}
for slot in offset..end {
if exclusive_requested {
Self::release_exclusive_slot(&mut state, slot, self.owner_id)?;
} else {
Self::release_shared_slot(&mut state, slot, self.owner_id)?;
}
}
Ok(())
}
fn shm_barrier(&self) {
fence(Ordering::SeqCst);
}
fn shm_unmap(&mut self, _cx: &Cx, delete: bool) -> Result<()> {
self.ensure_open()?;
self.release_shm_owner_state(delete)
}
}
impl crate::traits::AsyncVfsDataPath for WindowsFile {}
impl Drop for WindowsFile {
fn drop(&mut self) {
if !self.is_closed() || self.shm_state.is_some() {
let cx = Cx::new();
let _ = self.close(&cx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::process::Command;
use tempfile::tempdir;
struct TempPathCleanup(PathBuf);
impl Drop for TempPathCleanup {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
fn open_flags_create() -> VfsOpenFlags {
VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE
}
#[test]
fn test_windowsvfs_create_and_write() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("create_write.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.write(&cx, b"hello windows", 0).expect("write");
let mut buf = [0_u8; 13];
let n = file.read(&cx, &mut buf, 0).expect("read");
assert_eq!(n, 13);
assert_eq!(&buf, b"hello windows");
}
#[test]
fn test_windowsvfs_read_exact_at() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("read_at.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.write(&cx, b"0123456789", 0).expect("write");
let mut buf = [0_u8; 4];
let n = file.read(&cx, &mut buf, 3).expect("read");
assert_eq!(n, 4);
assert_eq!(&buf, b"3456");
}
#[test]
fn test_windowsvfs_write_all_at() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("write_at.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.write(&cx, b"abcdefghij", 0).expect("write base");
file.write(&cx, b"WXYZ", 2).expect("write overlay");
let mut buf = [0_u8; 10];
let n = file.read(&cx, &mut buf, 0).expect("read");
assert_eq!(n, 10);
assert_eq!(&buf, b"abWXYZghij");
}
#[test]
fn test_windowsvfs_file_size_and_truncate() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("size.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.write(&cx, &[7_u8; 4096], 0).expect("write");
assert_eq!(file.file_size(&cx).expect("size"), 4096);
file.truncate(&cx, 1024).expect("truncate");
assert_eq!(file.file_size(&cx).expect("size"), 1024);
}
#[test]
fn test_windowsvfs_file_size() {
test_windowsvfs_file_size_and_truncate();
}
#[test]
fn test_windowsvfs_truncate() {
test_windowsvfs_file_size_and_truncate();
}
#[test]
fn test_windowsvfs_shared_memory_create_and_cross_handle() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("shm.db");
let vfs = WindowsVfs::new();
let (mut file_a, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open A");
let (mut file_b, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open B");
let region_a = file_a.shm_map(&cx, 0, 32 * 1024, true).expect("map A");
{
let mut guard = region_a.lock();
guard[0] = 0xAA;
guard[1] = 0x55;
}
let region_b = file_b.shm_map(&cx, 0, 32 * 1024, false).expect("map B");
let guard = region_b.lock();
assert_eq!(guard[0], 0xAA);
assert_eq!(guard[1], 0x55);
drop(guard);
}
#[test]
fn test_windowsvfs_shared_memory_create() {
test_windowsvfs_shared_memory_create_and_cross_handle();
}
#[test]
fn test_windowsvfs_shared_memory_cross_handle() {
test_windowsvfs_shared_memory_create_and_cross_handle();
}
#[test]
fn test_windowsvfs_shm_resize_preserves_existing_mappings() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("shm_resize.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
let region_small = file.shm_map(&cx, 0, 32, true).expect("initial map");
region_small.write_u32_le(0, 0x1122_3344).unwrap();
let region_large = file.shm_map(&cx, 0, 64, true).expect("resized map");
region_large.write_u32_le(0, 0x5566_7788).unwrap();
region_large.write_u32_le(32, 0xAABB_CCDD).unwrap();
assert_eq!(
region_small.read_u32_le(0).unwrap(),
0x5566_7788,
"resizing must preserve shared backing for existing mappings"
);
assert_eq!(region_large.read_u32_le(32).unwrap(), 0xAABB_CCDD);
}
#[test]
fn test_windowsvfs_shm_map_extend_false_rejects_missing_without_side_effects() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("shm_missing_no_extend.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
let shm_path = file.shm_path.clone();
let err = file.shm_map(&cx, 2, 64, false).unwrap_err();
assert!(
matches!(err, FrankenError::CannotOpen { .. }),
"missing non-extend shm_map should report CannotOpen, got {err:?}"
);
assert!(
file.shm_state.is_none(),
"failed non-extend shm_map must not register shm owner state"
);
assert!(
windows_shm_table().get(&shm_path).unwrap().is_none(),
"failed non-extend shm_map must not create a shared state entry"
);
assert!(
!shm_path.exists(),
"failed non-extend shm_map must not create a -shm file"
);
}
#[test]
fn test_windowsvfs_reserved_lock_conflicts_across_handles() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("reserved_lock.db");
let vfs = WindowsVfs::new();
let (mut file_a, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open A");
let (mut file_b, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open B");
file_a.lock(&cx, LockLevel::Shared).expect("A shared");
file_a.lock(&cx, LockLevel::Reserved).expect("A reserved");
assert!(
!file_a.check_reserved_lock(&cx).unwrap(),
"a handle should not report its own RESERVED lock as external"
);
assert!(
file_b.check_reserved_lock(&cx).unwrap(),
"other handles must observe a RESERVED-or-higher sidecar lock"
);
assert!(
matches!(
file_b.lock(&cx, LockLevel::Reserved),
Err(FrankenError::Busy)
),
"second RESERVED locker must be rejected"
);
file_a.unlock(&cx, LockLevel::None).expect("release A");
file_b.lock(&cx, LockLevel::Reserved).expect("B reserved");
}
#[test]
fn test_windowsvfs_exclusive_lock_conflicts_with_other_shared_handle() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("exclusive_vs_shared.db");
let vfs = WindowsVfs::new();
let (mut file_a, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open A");
let (mut file_b, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open B");
file_a.lock(&cx, LockLevel::Shared).expect("A shared");
file_b.lock(&cx, LockLevel::Shared).expect("B shared");
assert!(
matches!(
file_a.lock(&cx, LockLevel::Exclusive),
Err(FrankenError::Busy)
),
"EXCLUSIVE must upgrade the shared sidecar and conflict with another SHARED holder"
);
assert_eq!(
file_a.lock_level,
LockLevel::Shared,
"failed EXCLUSIVE upgrade should roll back to the prior lock level"
);
file_a
.lock(&cx, LockLevel::Reserved)
.expect("failed exclusive upgrade must not strand RESERVED/PENDING sidecars");
}
#[test]
fn test_windowsvfs_shm_exclusive_unlock_preserves_prior_shared_lock() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("shm_lock_downgrade.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_SHARED)
.expect("acquire shared");
file.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
.expect("upgrade to exclusive");
file.shm_lock(&cx, 0, 1, SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE)
.expect("downgrade from exclusive");
file.shm_lock(&cx, 0, 1, SQLITE_SHM_UNLOCK | SQLITE_SHM_SHARED)
.expect("release preserved shared lock");
}
#[test]
fn test_windowsvfs_temp_file_auto_delete() {
let cx = Cx::new();
let vfs = WindowsVfs::new();
let flags = VfsOpenFlags::TEMP_DB
| VfsOpenFlags::CREATE
| VfsOpenFlags::READWRITE
| VfsOpenFlags::DELETEONCLOSE;
let (mut file, _) = vfs.open(&cx, None, flags).expect("open temp");
let temp_path = file.path.clone();
let lock_sidecars = windows_lock_sidecar_paths(&temp_path);
assert!(temp_path.exists());
for sidecar in &lock_sidecars {
assert!(
sidecar.exists(),
"temporary Windows VFS handle should create {}",
sidecar.display()
);
}
file.close(&cx).expect("close");
assert!(!temp_path.exists());
for sidecar in &lock_sidecars {
assert!(
!sidecar.exists(),
"temporary close should remove advisory lock sidecar {}",
sidecar.display()
);
}
}
#[test]
fn test_windowsvfs_temp_file_skips_existing_candidate() {
let cx = Cx::new();
let seed_base = 1_000_000_000_000_u64 + u64::from(std::process::id()) * 1_024;
let (seed, blocker, blocker_file) = (0_u64..1_024)
.find_map(|offset| {
let seed = seed_base + offset;
let blocker = env::temp_dir().join(format!("fsqlite-windows-{seed}.tmp"));
let mut blocker_options = windows_open_options();
blocker_options
.write(true)
.create_new(true)
.open(&blocker)
.ok()
.map(|file| (seed, blocker, file))
})
.expect("available temp candidate");
let _blocker_cleanup = TempPathCleanup(blocker.clone());
let mut blocker_file = blocker_file;
blocker_file
.write_all(b"existing temp candidate")
.expect("write existing temp candidate");
drop(blocker_file);
let vfs = WindowsVfs {
inner: Arc::new(Mutex::new(WindowsVfsInner { next_temp_id: seed })),
};
let flags = VfsOpenFlags::TEMP_DB
| VfsOpenFlags::CREATE
| VfsOpenFlags::READWRITE
| VfsOpenFlags::DELETEONCLOSE;
let (mut file, _) = vfs.open(&cx, None, flags).expect("open temp");
let opened_path = file.path.clone();
assert_ne!(
opened_path, blocker,
"anonymous temp open must not reuse an existing candidate path"
);
assert!(
blocker.exists(),
"temp collision handling must preserve the existing candidate file"
);
file.close(&cx).expect("close temp");
assert!(
!opened_path.exists(),
"delete-on-close should remove the actual temp file"
);
}
#[test]
fn test_windowsvfs_open_handles_block_delete_sharing() {
let dir = tempdir().expect("temp dir");
let path = dir.path().join("delete_sharing.db");
let mut options = windows_open_options();
let _file = options
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("open file without delete sharing");
assert!(
fs::remove_file(&path).is_err(),
"Windows VFS files must reject unlink while an open handle exists"
);
}
#[test]
fn test_windowsvfs_lock_open_failure_cleans_created_shared_sidecar() {
let dir = tempdir().expect("temp dir");
let path = dir.path().join("partial_lock_open.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
fs::create_dir(&reserved_path).expect("reserved sidecar blocker");
assert!(WindowsOsLockFiles::open(&path).is_err());
assert!(
!shared_path.exists(),
"failed lock setup should remove the shared sidecar it just created"
);
assert!(
reserved_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_open_failure_cleans_created_db_file() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("partial_vfs_open.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
fs::create_dir(&reserved_path).expect("reserved sidecar blocker");
let vfs = WindowsVfs::new();
let flags = open_flags_create() | VfsOpenFlags::EXCLUSIVE | VfsOpenFlags::DELETEONCLOSE;
assert!(vfs.open(&cx, Some(&path), flags).is_err());
assert!(
!path.exists(),
"failed exclusive create should remove the DB file it just created"
);
assert!(
!shared_path.exists(),
"failed lock setup should remove the shared sidecar it just created"
);
assert!(
reserved_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_plain_create_failure_cleans_created_db_file() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("partial_plain_create.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
fs::create_dir(&reserved_path).expect("reserved sidecar blocker");
let vfs = WindowsVfs::new();
assert!(vfs.open(&cx, Some(&path), open_flags_create()).is_err());
assert!(
!path.exists(),
"failed plain create should remove the DB file it just created"
);
assert!(
!shared_path.exists(),
"failed lock setup should remove the shared sidecar it just created"
);
assert!(
reserved_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_plain_create_failure_preserves_existing_db_file() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("existing_plain_create.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
fs::write(&path, b"existing db").expect("existing db");
fs::create_dir(&reserved_path).expect("reserved sidecar blocker");
let vfs = WindowsVfs::new();
assert!(vfs.open(&cx, Some(&path), open_flags_create()).is_err());
assert_eq!(
fs::read(&path).expect("read existing db"),
b"existing db",
"failed plain create must preserve an existing DB file"
);
assert!(
!shared_path.exists(),
"failed lock setup should remove only the shared sidecar it just created"
);
assert!(
reserved_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_open_failure_preserves_existing_sidecar() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("partial_vfs_open_existing_sidecar.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
fs::write(&shared_path, b"existing shared sidecar").expect("existing shared sidecar");
fs::create_dir(&reserved_path).expect("reserved sidecar blocker");
let vfs = WindowsVfs::new();
let flags = open_flags_create() | VfsOpenFlags::EXCLUSIVE | VfsOpenFlags::DELETEONCLOSE;
assert!(vfs.open(&cx, Some(&path), flags).is_err());
assert!(
!path.exists(),
"failed exclusive create should remove the DB file it just created"
);
assert!(
shared_path.exists(),
"failed VFS open must preserve a sidecar it did not create"
);
assert!(
reserved_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_lock_open_failure_preserves_existing_sidecars() {
let dir = tempdir().expect("temp dir");
let path = dir.path().join("existing_partial_lock_open.db");
let shared_path = sqlite_shared_lock_path(&path);
let reserved_path = sqlite_reserved_lock_path(&path);
let pending_path = sqlite_pending_lock_path(&path);
fs::write(&shared_path, b"existing shared sidecar").expect("existing shared sidecar");
fs::create_dir(&pending_path).expect("pending sidecar blocker");
assert!(WindowsOsLockFiles::open(&path).is_err());
assert!(
shared_path.exists(),
"failed lock setup must not remove a sidecar it did not create"
);
assert!(
!reserved_path.exists(),
"failed lock setup should remove the reserved sidecar it just created"
);
assert!(
pending_path.is_dir(),
"cleanup must not disturb the path that caused the open failure"
);
}
#[test]
fn test_windowsvfs_delete_on_close_is_idempotent() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("idempotent_close.db");
let vfs = WindowsVfs::new();
let flags = open_flags_create() | VfsOpenFlags::DELETEONCLOSE;
let (mut file, _) = vfs
.open(&cx, Some(&path), flags)
.expect("open delete-on-close file");
let shm_path = file.shm_path.clone();
let lock_sidecars = windows_lock_sidecar_paths(&path);
file.close(&cx).expect("first close");
assert!(!path.exists(), "first close should delete the DB file");
fs::write(&path, b"replacement db").expect("replacement db");
fs::write(&shm_path, b"replacement shm").expect("replacement shm");
for sidecar in &lock_sidecars {
fs::write(sidecar, b"replacement lock").expect("replacement sidecar");
}
file.close(&cx).expect("second close");
assert!(path.exists(), "second close must be a no-op");
assert!(
shm_path.exists(),
"second close must not delete replacement SHM"
);
for sidecar in &lock_sidecars {
assert!(
sidecar.exists(),
"second close must not delete replacement sidecar {}",
sidecar.display()
);
}
}
#[test]
fn test_windowsvfs_shm_rejects_use_after_close() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("closed_shm.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
file.close(&cx).expect("close file");
assert!(
matches!(
file.shm_map(&cx, 0, 32 * 1024, true),
Err(FrankenError::Internal(_))
),
"closed Windows handles must not recreate SHM state"
);
assert!(
matches!(
file.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_SHARED),
Err(FrankenError::Internal(_))
),
"closed Windows handles must reject SHM locks"
);
assert!(
matches!(file.shm_unmap(&cx, false), Err(FrankenError::Internal(_))),
"closed Windows handles must reject SHM unmap"
);
}
#[test]
fn test_windowsvfs_delete_removes_lock_sidecars() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("delete_sidecars.db");
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
let lock_sidecars = windows_lock_sidecar_paths(&path);
for sidecar in &lock_sidecars {
assert!(
sidecar.exists(),
"opening the Windows VFS handle should create {}",
sidecar.display()
);
}
file.close(&cx).expect("close file");
vfs.delete(&cx, &path, false).expect("delete file");
assert!(!path.exists(), "Vfs::delete should remove the main DB");
for sidecar in &lock_sidecars {
assert!(
!sidecar.exists(),
"Vfs::delete should remove advisory lock sidecar {}",
sidecar.display()
);
}
}
#[test]
fn test_windowsvfs_sector_size_detection() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("sector.db");
let vfs = WindowsVfs::new();
let (file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
let size = file.sector_size();
assert!(size.is_power_of_two());
assert!(size >= 512);
}
#[test]
fn test_windowsvfs_device_characteristics() {
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("iocap.db");
let vfs = WindowsVfs::new();
let (file, _) = vfs
.open(&cx, Some(&path), open_flags_create())
.expect("open file");
assert_eq!(
file.device_characteristics() & SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN,
SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
);
}
#[test]
fn test_e2e_windowsvfs_c_sqlite_interop() {
let sqlite_available = Command::new("sqlite3")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success());
if !sqlite_available {
return;
}
let cx = Cx::new();
let dir = tempdir().expect("temp dir");
let path = dir.path().join("interop.db");
let path_str = path.to_str().expect("path utf8");
let create_status = Command::new("sqlite3")
.arg(path_str)
.arg("CREATE TABLE t(x INTEGER); INSERT INTO t(x) VALUES (1),(2),(3);")
.status()
.expect("run sqlite3 create");
assert!(create_status.success());
let vfs = WindowsVfs::new();
let (mut file, _) = vfs
.open(
&cx,
Some(&path),
VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE,
)
.expect("open via windows vfs");
let mut header = [0_u8; 16];
let read = file.read(&cx, &mut header, 0).expect("read sqlite header");
assert_eq!(read, 16);
assert_eq!(&header, b"SQLite format 3\0");
file.close(&cx).expect("close vfs file");
let query_output = Command::new("sqlite3")
.arg(path_str)
.arg("SELECT count(*) FROM t;")
.output()
.expect("run sqlite3 query");
assert!(query_output.status.success());
let stdout = String::from_utf8(query_output.stdout).expect("utf8");
assert_eq!(stdout.trim(), "3");
}
#[test]
fn test_windowsvfs_cfg_gate() {
let _ = std::any::type_name::<WindowsVfs>();
let _ = std::any::type_name::<WindowsFile>();
}
}