use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use fsqlite_error::{FrankenError, Result};
use fsqlite_types::LockLevel;
use fsqlite_types::cx::Cx;
use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
use crate::shm::{
SQLITE_SHM_EXCLUSIVE, SQLITE_SHM_LOCK, SQLITE_SHM_SHARED, SQLITE_SHM_UNLOCK, ShmRegion,
WAL_TOTAL_LOCKS,
};
use crate::traits::{Vfs, VfsFile};
#[derive(Debug, Default)]
struct FileStorage {
data: Vec<u8>,
}
#[derive(Debug, Default)]
struct MemoryVfsInner {
files: HashMap<PathBuf, Arc<Mutex<FileStorage>>>,
shm: HashMap<PathBuf, Arc<Mutex<MemoryShmInfo>>>,
next_temp_id: u64,
}
#[derive(Debug, Default)]
struct ShmSlotState {
shared_holders: HashMap<u64, u32>,
exclusive_owner: Option<u64>,
}
#[derive(Debug)]
struct MemoryShmInfo {
regions: HashMap<u32, ShmRegion>,
slots: Vec<ShmSlotState>,
owner_refs: HashMap<u64, u32>,
}
impl MemoryShmInfo {
fn new() -> Self {
let slot_count =
usize::try_from(WAL_TOTAL_LOCKS.saturating_add(1)).expect("WAL lock count fits usize");
Self {
regions: HashMap::new(),
slots: std::iter::repeat_with(ShmSlotState::default)
.take(slot_count)
.collect(),
owner_refs: HashMap::new(),
}
}
}
static SHM_OWNER_SEQ: AtomicU64 = AtomicU64::new(1);
fn next_shm_owner_id() -> u64 {
SHM_OWNER_SEQ.fetch_add(1, Ordering::Relaxed)
}
fn sqlite_shm_path(path: &Path) -> PathBuf {
let mut shm = OsString::from(path.as_os_str());
shm.push("-shm");
PathBuf::from(shm)
}
#[derive(Debug, Clone, Default)]
pub struct MemoryVfs {
inner: Arc<Mutex<MemoryVfsInner>>,
}
impl MemoryVfs {
pub fn new() -> Self {
Self::default()
}
}
fn lock_err() -> FrankenError {
FrankenError::internal("MemoryVfs lock poisoned")
}
fn checkpoint_or_abort(cx: &Cx) -> Result<()> {
cx.checkpoint().map_err(|_| FrankenError::Abort)
}
fn u64_to_usize(value: u64, what: &str) -> Result<usize> {
usize::try_from(value).map_err(|_| FrankenError::OutOfRange {
what: what.to_string(),
value: value.to_string(),
})
}
impl Vfs for MemoryVfs {
type File = MemoryFile;
fn name(&self) -> &'static str {
"memory"
}
#[allow(clippy::significant_drop_tightening)]
fn open(
&self,
cx: &Cx,
path: Option<&Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
checkpoint_or_abort(cx)?;
let mut inner = self.inner.lock().map_err(|_| lock_err())?;
let is_anonymous_temp = path.is_none();
let resolved_path = if let Some(p) = path {
p.to_path_buf()
} else {
let id = inner.next_temp_id;
inner.next_temp_id += 1;
PathBuf::from(format!("__temp_{id}__"))
};
let is_create = flags.contains(VfsOpenFlags::CREATE);
let storage = if let Some(existing) = inner.files.get(&resolved_path) {
Arc::clone(existing)
} else if is_create {
let storage = Arc::new(Mutex::new(FileStorage::default()));
inner
.files
.insert(resolved_path.clone(), Arc::clone(&storage));
storage
} else {
return Err(FrankenError::CannotOpen {
path: resolved_path,
});
};
drop(inner);
let shm_owner_id = next_shm_owner_id();
let shm_path = sqlite_shm_path(&resolved_path);
let file = MemoryFile {
path: resolved_path,
storage,
lock_level: LockLevel::None,
delete_on_close: flags.contains(VfsOpenFlags::DELETEONCLOSE) || is_anonymous_temp,
vfs: Arc::clone(&self.inner),
shm_owner_id,
shm_path,
shm_info: None,
};
let mut out_flags = flags;
if is_create {
out_flags |= VfsOpenFlags::READWRITE;
}
Ok((file, out_flags))
}
fn delete(&self, _cx: &Cx, path: &Path, _sync_dir: bool) -> Result<()> {
self.inner
.lock()
.map_err(|_| lock_err())?
.files
.remove(path);
Ok(())
}
fn access(&self, _cx: &Cx, path: &Path, _flags: AccessFlags) -> Result<bool> {
Ok(self
.inner
.lock()
.map_err(|_| lock_err())?
.files
.contains_key(path))
}
fn full_pathname(&self, _cx: &Cx, path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(env::current_dir()?.join(path))
}
}
}
#[derive(Debug)]
pub struct MemoryFile {
path: PathBuf,
storage: Arc<Mutex<FileStorage>>,
lock_level: LockLevel,
delete_on_close: bool,
vfs: Arc<Mutex<MemoryVfsInner>>,
shm_owner_id: u64,
shm_path: PathBuf,
shm_info: Option<Arc<Mutex<MemoryShmInfo>>>,
}
impl MemoryFile {
fn ensure_shm_info(&mut self) -> Result<Arc<Mutex<MemoryShmInfo>>> {
if let Some(info) = &self.shm_info {
return Ok(Arc::clone(info));
}
let info = {
let mut inner = self.vfs.lock().map_err(|_| lock_err())?;
Arc::clone(
inner
.shm
.entry(self.shm_path.clone())
.or_insert_with(|| Arc::new(Mutex::new(MemoryShmInfo::new()))),
)
};
{
let mut guard = info.lock().map_err(|_| lock_err())?;
*guard.owner_refs.entry(self.shm_owner_id).or_insert(0) += 1;
}
self.shm_info = Some(Arc::clone(&info));
Ok(info)
}
fn release_shm_owner_state(&mut self, _delete: bool) -> Result<()> {
let Some(info_arc) = self.shm_info.take() else {
return Ok(());
};
let orphaned = {
let mut info = info_arc.lock().map_err(|_| lock_err())?;
for slot_state in &mut info.slots {
if slot_state.exclusive_owner == Some(self.shm_owner_id) {
slot_state.exclusive_owner = None;
}
slot_state.shared_holders.remove(&self.shm_owner_id);
}
if let Some(count) = info.owner_refs.get_mut(&self.shm_owner_id) {
if *count > 1 {
*count -= 1;
} else {
info.owner_refs.remove(&self.shm_owner_id);
}
}
info.owner_refs.is_empty()
};
if orphaned {
let mut inner = self.vfs.lock().map_err(|_| lock_err())?;
inner.shm.remove(&self.shm_path);
}
Ok(())
}
fn validate_shm_request(offset: u32, n: u32) -> Result<()> {
if n == 0 {
return Err(FrankenError::LockFailed {
detail: "shm_lock called with n=0".to_string(),
});
}
let Some(end) = offset.checked_add(n) else {
return Err(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(())
}
fn acquire_shm_shared_slot(&self, info: &mut MemoryShmInfo, slot: u32) -> Result<()> {
let slot_idx = usize::try_from(slot).map_err(|_| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
let slot_state = info
.slots
.get_mut(slot_idx)
.ok_or_else(|| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
if let Some(owner) = slot_state.exclusive_owner {
if owner != self.shm_owner_id {
return Err(FrankenError::Busy);
}
return Ok(());
}
*slot_state
.shared_holders
.entry(self.shm_owner_id)
.or_insert(0) += 1;
Ok(())
}
fn release_shm_shared_slot(&self, info: &mut MemoryShmInfo, slot: u32) -> Result<()> {
let slot_idx = usize::try_from(slot).map_err(|_| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
let slot_state = info
.slots
.get_mut(slot_idx)
.ok_or_else(|| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
let Some(holder_count) = slot_state.shared_holders.get_mut(&self.shm_owner_id) else {
return Err(FrankenError::LockFailed {
detail: format!(
"owner {} does not hold shared slot {slot}",
self.shm_owner_id
),
});
};
if *holder_count > 1 {
*holder_count -= 1;
} else {
slot_state.shared_holders.remove(&self.shm_owner_id);
}
Ok(())
}
fn acquire_shm_exclusive_slot(&self, info: &mut MemoryShmInfo, slot: u32) -> Result<()> {
let slot_idx = usize::try_from(slot).map_err(|_| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
let slot_state = info
.slots
.get_mut(slot_idx)
.ok_or_else(|| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
if slot_state.exclusive_owner == Some(self.shm_owner_id) {
return Ok(());
}
if slot_state.exclusive_owner.is_some() {
return Err(FrankenError::Busy);
}
let shared_from_others = slot_state
.shared_holders
.iter()
.any(|(owner, count)| *owner != self.shm_owner_id && *count > 0);
if shared_from_others {
return Err(FrankenError::Busy);
}
slot_state.shared_holders.remove(&self.shm_owner_id);
slot_state.exclusive_owner = Some(self.shm_owner_id);
Ok(())
}
fn release_shm_exclusive_slot(&self, info: &mut MemoryShmInfo, slot: u32) -> Result<()> {
let slot_idx = usize::try_from(slot).map_err(|_| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
let slot_state = info
.slots
.get_mut(slot_idx)
.ok_or_else(|| FrankenError::LockFailed {
detail: format!("invalid SHM slot {slot}"),
})?;
if slot_state.exclusive_owner != Some(self.shm_owner_id) {
return Err(FrankenError::LockFailed {
detail: format!(
"owner {} does not hold exclusive slot {slot}",
self.shm_owner_id
),
});
}
slot_state.exclusive_owner = None;
Ok(())
}
}
impl VfsFile for MemoryFile {
fn close(&mut self, cx: &Cx) -> Result<()> {
self.unlock(cx, LockLevel::None)?;
self.release_shm_owner_state(self.delete_on_close)?;
if self.delete_on_close {
self.vfs
.lock()
.map_err(|_| lock_err())?
.files
.remove(&self.path);
}
Ok(())
}
fn read(&mut self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize> {
checkpoint_or_abort(cx)?;
let storage = self.storage.lock().map_err(|_| lock_err())?;
let offset = u64_to_usize(offset, "read offset")?;
let file_len = storage.data.len();
if offset >= file_len {
drop(storage);
buf.fill(0);
return Ok(0);
}
let available = file_len - offset;
let to_read = buf.len().min(available);
buf[..to_read].copy_from_slice(&storage.data[offset..offset + to_read]);
drop(storage);
if to_read < buf.len() {
buf[to_read..].fill(0);
}
Ok(to_read)
}
#[allow(clippy::significant_drop_tightening)]
fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()> {
checkpoint_or_abort(cx)?;
let mut storage = self.storage.lock().map_err(|_| lock_err())?;
let offset = u64_to_usize(offset, "write offset")?;
let end = offset
.checked_add(buf.len())
.ok_or_else(|| FrankenError::OutOfRange {
what: "write end offset".to_string(),
value: format!("offset={offset}, len={}", buf.len()),
})?;
if end > storage.data.len() {
storage.data.resize(end, 0);
}
storage.data[offset..end].copy_from_slice(buf);
Ok(())
}
fn truncate(&mut self, _cx: &Cx, size: u64) -> Result<()> {
let size = u64_to_usize(size, "truncate size")?;
let mut storage = self.storage.lock().map_err(|_| lock_err())?;
storage.data.resize(size, 0);
if storage.data.capacity() > 1024 && size < storage.data.capacity() / 2 {
storage.data.shrink_to_fit();
}
drop(storage);
Ok(())
}
fn sync(&mut self, _cx: &Cx, _flags: SyncFlags) -> Result<()> {
Ok(())
}
fn file_size(&self, _cx: &Cx) -> Result<u64> {
Ok(self.storage.lock().map_err(|_| lock_err())?.data.len() as u64)
}
fn lock(&mut self, _cx: &Cx, level: LockLevel) -> Result<()> {
if self.lock_level < level {
self.lock_level = level;
}
Ok(())
}
fn unlock(&mut self, _cx: &Cx, level: LockLevel) -> Result<()> {
if self.lock_level > level {
self.lock_level = level;
}
Ok(())
}
fn check_reserved_lock(&self, _cx: &Cx) -> Result<bool> {
Ok(false)
}
fn shm_map(
&mut self,
cx: &Cx,
region: u32,
size: u32,
extend: bool,
) -> Result<crate::shm::ShmRegion> {
checkpoint_or_abort(cx)?;
if size == 0 {
return Err(FrankenError::LockFailed {
detail: "shm_map size must be > 0".to_string(),
});
}
let shm_info = self.ensure_shm_info()?;
let mut info = shm_info.lock().map_err(|_| lock_err())?;
if let Some(existing) = info.regions.get(®ion).cloned() {
let requested_size = usize::try_from(size).map_err(|_| FrankenError::LockFailed {
detail: format!("shm_map size too large: {size}"),
})?;
if existing.len() >= requested_size {
drop(info);
return Ok(existing);
}
if !extend {
drop(info);
return Err(FrankenError::LockFailed {
detail: format!(
"shm region {region} is {} bytes, requested {requested_size} bytes without extend",
existing.len()
),
});
}
let replacement = ShmRegion::new(requested_size);
{
let existing_guard = existing.lock();
let mut replacement_guard = replacement.lock();
let copy_len = existing_guard.len().min(replacement_guard.len());
replacement_guard[..copy_len].copy_from_slice(&existing_guard[..copy_len]);
}
info.regions.insert(region, replacement.clone());
drop(info);
return Ok(replacement);
}
if !extend {
drop(info);
return Err(FrankenError::CannotOpen {
path: self.shm_path.clone(),
});
}
let map_size = usize::try_from(size).map_err(|_| FrankenError::LockFailed {
detail: format!("shm_map size too large: {size}"),
})?;
let new_region = ShmRegion::new(map_size);
info.regions.insert(region, new_region.clone());
drop(info);
Ok(new_region)
}
fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
checkpoint_or_abort(cx)?;
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_mode = flags & SQLITE_SHM_SHARED != 0;
let exclusive_mode = flags & SQLITE_SHM_EXCLUSIVE != 0;
if shared_mode == exclusive_mode {
return Err(FrankenError::LockFailed {
detail: "invalid shm_lock flags (must set exactly one of SHARED/EXCLUSIVE)"
.to_string(),
});
}
let shm_info = self.ensure_shm_info()?;
let mut info = shm_info.lock().map_err(|_| lock_err())?;
if lock_requested {
let mut acquired = Vec::new();
for slot in offset..offset + n {
let result = if exclusive_mode {
self.acquire_shm_exclusive_slot(&mut info, slot)
} else {
self.acquire_shm_shared_slot(&mut info, slot)
};
match result {
Ok(()) => acquired.push(slot),
Err(err) => {
for acquired_slot in acquired.into_iter().rev() {
if exclusive_mode {
let _ = self.release_shm_exclusive_slot(&mut info, acquired_slot);
} else {
let _ = self.release_shm_shared_slot(&mut info, acquired_slot);
}
}
return Err(err);
}
}
}
return Ok(());
}
for slot in offset..offset + n {
if exclusive_mode {
self.release_shm_exclusive_slot(&mut info, slot)?;
} else {
self.release_shm_shared_slot(&mut info, slot)?;
}
}
Ok(())
}
fn shm_barrier(&self) {}
fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
checkpoint_or_abort(cx)?;
self.release_shm_owner_state(delete)
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
fn make_vfs() -> MemoryVfs {
MemoryVfs::new()
}
#[test]
fn create_and_read_file() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("test.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, b"hello", 0).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 5);
let mut buf = [0u8; 5];
let n = file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 5);
assert_eq!(&buf, b"hello");
}
#[test]
fn test_memory_vfs_write_read_1mb() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("one_mb.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let payload = (0..(1024 * 1024))
.map(|idx| u8::try_from(idx % 251).expect("mod value must fit in u8"))
.collect::<Vec<_>>();
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, &payload, 0).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), payload.len() as u64);
let mut roundtrip = vec![0_u8; payload.len()];
let read = file.read(&cx, &mut roundtrip, 0).unwrap();
assert_eq!(read, payload.len());
assert_eq!(roundtrip, payload);
}
#[test]
fn read_past_end_zeroes() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("test.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, b"hi", 0).unwrap();
let mut buf = [0xFFu8; 10];
let n = file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 2);
assert_eq!(&buf[..2], b"hi");
assert!(buf[2..].iter().all(|&b| b == 0));
}
#[test]
fn read_from_empty_file() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("empty.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
let mut buf = [0xFFu8; 4];
let n = file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 0);
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn write_extends_file() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("test.db")), flags).unwrap();
file.write(&cx, b"world", 10).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 15);
let mut buf = [0xFFu8; 15];
file.read(&cx, &mut buf, 0).unwrap();
assert!(buf[..10].iter().all(|&b| b == 0));
assert_eq!(&buf[10..], b"world");
}
#[test]
fn truncate() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("test.db")), flags).unwrap();
file.write(&cx, b"hello world", 0).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 11);
file.truncate(&cx, 5).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 5);
let mut buf = [0u8; 5];
file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"hello");
}
#[test]
fn test_memory_vfs_truncate() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let payload = vec![0xAB_u8; 1024 * 1024];
let (mut file, _) = vfs
.open(&cx, Some(Path::new("truncate_1mb.db")), flags)
.unwrap();
file.write(&cx, &payload, 0).unwrap();
file.truncate(&cx, 512 * 1024).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 512 * 1024);
let mut buf = vec![0_u8; 512 * 1024];
let n = file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 512 * 1024);
assert!(buf.iter().all(|byte| *byte == 0xAB));
}
#[test]
fn open_nonexistent_without_create_fails() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let result = vfs.open(&cx, Some(Path::new("nope.db")), flags);
assert!(result.is_err());
}
#[test]
fn delete_file() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("test.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, b"data", 0).unwrap();
file.close(&cx).unwrap();
assert!(vfs.access(&cx, path, AccessFlags::EXISTS).unwrap());
vfs.delete(&cx, path, false).unwrap();
assert!(!vfs.access(&cx, path, AccessFlags::EXISTS).unwrap());
}
#[test]
fn delete_on_close() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("temp.db");
let flags = VfsOpenFlags::MAIN_DB
| VfsOpenFlags::CREATE
| VfsOpenFlags::READWRITE
| VfsOpenFlags::DELETEONCLOSE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, b"temp data", 0).unwrap();
assert!(vfs.access(&cx, path, AccessFlags::EXISTS).unwrap());
file.close(&cx).unwrap();
assert!(!vfs.access(&cx, path, AccessFlags::EXISTS).unwrap());
}
#[test]
fn temp_file_auto_naming() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::TEMP_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file1, _) = vfs.open(&cx, None, flags).unwrap();
let (mut file2, _) = vfs.open(&cx, None, flags).unwrap();
file1.write(&cx, b"file1", 0).unwrap();
file2.write(&cx, b"file2", 0).unwrap();
let mut buf = [0u8; 5];
file1.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"file1");
file2.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"file2");
assert_eq!(vfs.inner.lock().unwrap().files.len(), 2);
file1.close(&cx).unwrap();
file2.close(&cx).unwrap();
assert_eq!(
vfs.inner.lock().unwrap().files.len(),
0,
"anonymous temp files must be deleted on close"
);
}
#[test]
fn shared_file_across_handles() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("shared.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file1, _) = vfs.open(&cx, Some(path), flags).unwrap();
file1.write(&cx, b"shared data", 0).unwrap();
let open_flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let (mut file2, _) = vfs.open(&cx, Some(path), open_flags).unwrap();
let mut buf = [0u8; 11];
let n = file2.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 11);
assert_eq!(&buf, b"shared data");
}
#[test]
fn full_pathname() {
let cx = Cx::new();
let vfs = make_vfs();
let resolved = vfs.full_pathname(&cx, Path::new("test.db")).unwrap();
assert!(resolved.is_absolute());
let already_abs_input = std::env::current_dir().unwrap().join("tmp").join("test.db");
let already_abs = vfs.full_pathname(&cx, &already_abs_input).unwrap();
assert_eq!(already_abs, already_abs_input);
}
#[test]
fn locking() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("lock.db")), flags).unwrap();
file.lock(&cx, LockLevel::Shared).unwrap();
file.lock(&cx, LockLevel::Reserved).unwrap();
file.lock(&cx, LockLevel::Exclusive).unwrap();
assert!(!file.check_reserved_lock(&cx).unwrap());
file.unlock(&cx, LockLevel::Shared).unwrap();
file.unlock(&cx, LockLevel::None).unwrap();
}
#[test]
fn sync_is_noop() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("sync.db")), flags).unwrap();
file.sync(&cx, SyncFlags::FULL).unwrap();
}
#[test]
fn write_overwrite() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("test.db")), flags).unwrap();
file.write(&cx, b"AAAAAAAAAA", 0).unwrap();
file.write(&cx, b"BB", 3).unwrap();
let mut buf = [0u8; 10];
file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"AAABBAAAAA");
}
#[test]
fn vfs_name() {
let vfs = make_vfs();
assert_eq!(vfs.name(), "memory");
}
#[test]
fn vfs_default_current_time() {
let cx = Cx::new();
let vfs = make_vfs();
cx.set_unix_millis_for_testing(1_700_000_000_000);
let time = vfs.current_time(&cx);
#[allow(clippy::cast_precision_loss)]
let expected = 2_440_587.5 + ((1_700_000_000_000_f64 / 1000.0) / 86_400.0);
assert!(
(time - expected).abs() < 1e-9,
"unexpected julian day: got={time} expected≈{expected}"
);
}
#[test]
fn vfs_default_randomness() {
let cx = Cx::new();
let vfs = make_vfs();
let mut buf1 = [0u8; 16];
let mut buf2 = [0u8; 16];
vfs.randomness(&cx, &mut buf1);
vfs.randomness(&cx, &mut buf2);
assert_ne!(buf1, buf2);
}
#[test]
fn page_read_write_roundtrip() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("pages.db")), flags).unwrap();
let page1 = vec![0xAA_u8; 4096];
let page2 = vec![0xBB_u8; 4096];
file.write(&cx, &page1, 0).unwrap();
file.write(&cx, &page2, 4096).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 8192);
let mut buf = vec![0u8; 4096];
file.read(&cx, &mut buf, 0).unwrap();
assert!(buf.iter().all(|&b| b == 0xAA));
file.read(&cx, &mut buf, 4096).unwrap();
assert!(buf.iter().all(|&b| b == 0xBB));
}
#[test]
fn clone_vfs_shares_state() {
let cx = Cx::new();
let vfs1 = make_vfs();
let vfs2 = vfs1.clone();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs1.open(&cx, Some(Path::new("shared.db")), flags).unwrap();
file.write(&cx, b"from vfs1", 0).unwrap();
let open_flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let (mut file2, _) = vfs2
.open(&cx, Some(Path::new("shared.db")), open_flags)
.unwrap();
let mut buf = [0u8; 9];
let n = file2.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 9);
assert_eq!(&buf, b"from vfs1");
}
#[test]
fn write_zero_bytes() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("zero.db")), flags).unwrap();
file.write(&cx, b"abc", 0).unwrap();
file.write(&cx, b"", 0).unwrap(); assert_eq!(file.file_size(&cx).unwrap(), 3);
let mut buf = [0u8; 3];
file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"abc");
}
#[test]
fn write_offset_overflow_returns_error() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs
.open(&cx, Some(Path::new("overflow_offset.db")), flags)
.unwrap();
let err = file.write(&cx, b"ab", u64::MAX).unwrap_err();
assert!(matches!(err, FrankenError::OutOfRange { .. }));
}
#[test]
fn read_zero_bytes() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("rz.db")), flags).unwrap();
file.write(&cx, b"data", 0).unwrap();
let mut buf = [];
let n = file.read(&cx, &mut buf, 0).unwrap();
assert_eq!(n, 0);
}
#[test]
fn read_at_exact_end() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("end.db")), flags).unwrap();
file.write(&cx, b"12345", 0).unwrap();
let mut buf = [0xFFu8; 4];
let n = file.read(&cx, &mut buf, 5).unwrap();
assert_eq!(n, 0);
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn truncate_to_zero() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("tz.db")), flags).unwrap();
file.write(&cx, b"content", 0).unwrap();
file.truncate(&cx, 0).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 0);
}
#[test]
fn truncate_beyond_size_is_noop() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("tb.db")), flags).unwrap();
file.write(&cx, b"hi", 0).unwrap();
file.truncate(&cx, 100).unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 2);
}
#[test]
fn close_without_delete_preserves_file() {
let cx = Cx::new();
let vfs = make_vfs();
let path = Path::new("keep.db");
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(path), flags).unwrap();
file.write(&cx, b"persist", 0).unwrap();
file.close(&cx).unwrap();
assert!(vfs.access(&cx, path, AccessFlags::EXISTS).unwrap());
}
#[test]
fn shm_map_roundtrip_shared_between_handles() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file1, _) = vfs.open(&cx, Some(Path::new("shm.db")), flags).unwrap();
let (mut file2, _) = vfs.open(&cx, Some(Path::new("shm.db")), flags).unwrap();
let region1 = file1.shm_map(&cx, 0, 64, true).unwrap();
{
let mut g = region1.lock();
g[0] = 0xAA;
g[1] = 0xBB;
}
let region2 = file2.shm_map(&cx, 0, 64, true).unwrap();
let (b0, b1) = {
let g2 = region2.lock();
(g2[0], g2[1])
};
assert_eq!(b0, 0xAA);
assert_eq!(b1, 0xBB);
}
#[test]
fn shm_map_extend_false_rejects_missing_region() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs
.open(&cx, Some(Path::new("shm_missing.db")), flags)
.unwrap();
let err = file.shm_map(&cx, 2, 64, false).unwrap_err();
assert!(matches!(err, FrankenError::CannotOpen { .. }));
}
#[test]
fn shm_map_existing_region_grows_on_larger_extend_request() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs
.open(&cx, Some(Path::new("shm_grow.db")), flags)
.unwrap();
let region_small = file.shm_map(&cx, 0, 64, true).unwrap();
{
let mut guard = region_small.lock();
guard[0] = 0x11;
guard[63] = 0x22;
}
let region_big = file.shm_map(&cx, 0, 128, true).unwrap();
assert_eq!(region_big.len(), 128);
{
let guard = region_big.lock();
assert_eq!(guard[0], 0x11);
assert_eq!(guard[63], 0x22);
assert_eq!(guard[127], 0x00);
drop(guard);
}
}
#[test]
fn shm_lock_shared_and_exclusive_conflict() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file1, _) = vfs
.open(&cx, Some(Path::new("shm_lock.db")), flags)
.unwrap();
let (mut file2, _) = vfs
.open(&cx, Some(Path::new("shm_lock.db")), flags)
.unwrap();
file1
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_SHARED)
.unwrap();
file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_SHARED)
.unwrap();
let err = file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
.unwrap_err();
assert!(matches!(err, FrankenError::Busy));
file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_UNLOCK | SQLITE_SHM_SHARED)
.unwrap();
file1
.shm_lock(&cx, 0, 1, SQLITE_SHM_UNLOCK | SQLITE_SHM_SHARED)
.unwrap();
file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
.unwrap();
file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE)
.unwrap();
}
#[test]
fn close_releases_shm_locks() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file1, _) = vfs
.open(&cx, Some(Path::new("shm_close.db")), flags)
.unwrap();
let (mut file2, _) = vfs
.open(&cx, Some(Path::new("shm_close.db")), flags)
.unwrap();
file1
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
.unwrap();
file1.close(&cx).unwrap();
file2
.shm_lock(&cx, 0, 1, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
.unwrap();
}
#[test]
fn shm_barrier_is_noop() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (file, _) = vfs.open(&cx, Some(Path::new("shmb.db")), flags).unwrap();
file.shm_barrier(); }
#[test]
fn shm_unmap_succeeds() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("shmu.db")), flags).unwrap();
file.shm_unmap(&cx, false).unwrap();
file.shm_unmap(&cx, true).unwrap(); }
#[test]
fn delete_nonexistent_is_silent() {
let cx = Cx::new();
let vfs = make_vfs();
vfs.delete(&cx, Path::new("ghost.db"), false).unwrap();
}
#[test]
fn new_file_size_is_zero() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (file, _) = vfs
.open(&cx, Some(Path::new("empty_sz.db")), flags)
.unwrap();
assert_eq!(file.file_size(&cx).unwrap(), 0);
}
#[test]
fn open_with_create_adds_readwrite_flag() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE;
let (_, out_flags) = vfs.open(&cx, Some(Path::new("flag.db")), flags).unwrap();
assert!(out_flags.contains(VfsOpenFlags::READWRITE));
}
#[test]
fn access_readwrite_on_existing_file() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("acc.db")), flags).unwrap();
file.write(&cx, b"test", 0).unwrap();
assert!(
vfs.access(&cx, Path::new("acc.db"), AccessFlags::READWRITE)
.unwrap()
);
}
#[test]
fn access_nonexistent() {
let cx = Cx::new();
let vfs = make_vfs();
assert!(
!vfs.access(&cx, Path::new("nope.db"), AccessFlags::EXISTS)
.unwrap()
);
}
#[test]
fn unlock_below_current_level_is_noop() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("unlock.db")), flags).unwrap();
file.lock(&cx, LockLevel::Shared).unwrap();
file.unlock(&cx, LockLevel::Exclusive).unwrap();
}
#[test]
fn multiple_temp_files_distinct() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::TEMP_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut f1, _) = vfs.open(&cx, None, flags).unwrap();
let (mut f2, _) = vfs.open(&cx, None, flags).unwrap();
let (mut f3, _) = vfs.open(&cx, None, flags).unwrap();
f1.write(&cx, b"one", 0).unwrap();
f2.write(&cx, b"two", 0).unwrap();
f3.write(&cx, b"three", 0).unwrap();
let mut buf = [0u8; 5];
f1.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf[..3], b"one");
f2.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf[..3], b"two");
f3.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf[..5], b"three");
}
#[test]
fn full_pathname_relative_gets_root_prefix() {
let cx = Cx::new();
let vfs = make_vfs();
let result = vfs.full_pathname(&cx, Path::new("foo/bar.db")).unwrap();
let expected = std::env::current_dir().unwrap().join("foo/bar.db");
assert_eq!(result, expected);
}
#[test]
fn memory_vfs_default_trait() {
let vfs = MemoryVfs::default();
assert_eq!(vfs.name(), "memory");
}
#[test]
fn concurrent_write_via_shared_handle() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut f1, _) = vfs.open(&cx, Some(Path::new("conc.db")), flags).unwrap();
f1.write(&cx, b"AAAA", 0).unwrap();
let open_flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let (mut f2, _) = vfs
.open(&cx, Some(Path::new("conc.db")), open_flags)
.unwrap();
f2.write(&cx, b"BB", 1).unwrap();
let mut buf = [0u8; 4];
f1.read(&cx, &mut buf, 0).unwrap();
assert_eq!(&buf, b"ABBA");
}
#[test]
fn close_resets_lock_to_none() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("clr.db")), flags).unwrap();
file.lock(&cx, LockLevel::Exclusive).unwrap();
file.close(&cx).unwrap();
assert_eq!(file.lock_level, LockLevel::None);
}
#[test]
fn lock_full_escalation_and_downgrade() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("esc.db")), flags).unwrap();
file.lock(&cx, LockLevel::Shared).unwrap();
file.lock(&cx, LockLevel::Reserved).unwrap();
file.lock(&cx, LockLevel::Pending).unwrap();
file.lock(&cx, LockLevel::Exclusive).unwrap();
file.unlock(&cx, LockLevel::Shared).unwrap();
file.unlock(&cx, LockLevel::None).unwrap();
assert!(!file.check_reserved_lock(&cx).unwrap());
}
#[test]
fn sector_size_and_device_characteristics_defaults() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (file, _) = vfs.open(&cx, Some(Path::new("dev.db")), flags).unwrap();
assert_eq!(file.sector_size(), 4096);
assert_eq!(file.device_characteristics(), 0);
}
#[test]
fn sync_with_different_flags() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(&cx, Some(Path::new("sync2.db")), flags).unwrap();
file.sync(&cx, SyncFlags::NORMAL).unwrap();
file.sync(&cx, SyncFlags::FULL).unwrap();
file.sync(&cx, SyncFlags::DATAONLY).unwrap();
}
#[test]
fn test_locking_contention() {
let cx = Cx::new();
let vfs = make_vfs();
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE;
let path = Path::new("contention.db");
let (mut f1, _) = vfs.open(&cx, Some(path), flags).unwrap();
let (mut f2, _) = vfs.open(&cx, Some(path), flags).unwrap();
f1.lock(&cx, LockLevel::Shared).unwrap();
f2.lock(&cx, LockLevel::Shared).unwrap();
f1.lock(&cx, LockLevel::Reserved).unwrap();
f2.lock(&cx, LockLevel::Reserved).unwrap();
assert!(!f2.check_reserved_lock(&cx).unwrap());
assert!(!f1.check_reserved_lock(&cx).unwrap());
f1.lock(&cx, LockLevel::Exclusive).unwrap();
f2.lock(&cx, LockLevel::Exclusive).unwrap();
f1.lock(&cx, LockLevel::Shared).unwrap();
assert_eq!(f1.lock_level, LockLevel::Exclusive);
f1.unlock(&cx, LockLevel::None).unwrap();
assert_eq!(f1.lock_level, LockLevel::None);
f2.lock(&cx, LockLevel::Shared).unwrap();
assert_eq!(f2.lock_level, LockLevel::Exclusive);
f2.unlock(&cx, LockLevel::Shared).unwrap();
assert_eq!(f2.lock_level, LockLevel::Shared);
}
}