#![forbid(unsafe_code)]
use std::path::Path;
use crate::store::transaction::CrashHooks;
use crate::store::{NewEntry, Store, StoreConfig, StoreError};
pub const BLOCK_SIZE: u64 = 4096;
pub const DEVICE_PREFIX: &str = ".ublk-";
pub struct BlockStore {
store: Store,
ino: u64,
blocks: u64,
}
impl BlockStore {
pub fn open_or_create(
dir: &Path,
config: &StoreConfig,
name: &str,
capacity_bytes: u64,
) -> Result<Self, StoreError> {
if name.is_empty() || name.contains('/') || name.len() > 128 {
return Err(StoreError::Config("invalid ublk device name".into()));
}
let dev_name = format!("{DEVICE_PREFIX}{name}");
let mut store = if dir.join("superblock").exists() {
Store::open(dir, config)?
} else {
Store::create(
dir,
config,
crate::core::extent::ChunkId::of(name.as_bytes()).as_bytes()[..16]
.try_into()
.expect("16 bytes"),
)?
};
let ino = match store.dir_lookup(1, dev_name.as_bytes())? {
Some(e) => e.ino,
None => store.create_entry(
1,
dev_name.as_bytes(),
NewEntry::file(
0o600,
crate::store::current_uid(),
crate::store::current_gid(),
),
&CrashHooks::none(),
)?,
};
let blocks = capacity_bytes.div_ceil(BLOCK_SIZE);
let inode = store
.get_inode(ino)?
.ok_or_else(|| StoreError::Invariant(format!("device inode {ino} missing")))?;
if inode.size != blocks * BLOCK_SIZE {
store.truncate_file(ino, blocks * BLOCK_SIZE)?;
}
store.durability_barrier(&CrashHooks::none())?;
Ok(Self { store, ino, blocks })
}
pub fn open(dir: &Path, config: &StoreConfig, name: &str) -> Result<Self, StoreError> {
let dev_name = format!("{DEVICE_PREFIX}{name}");
let store = Store::open(dir, config)?;
let ino = store
.dir_lookup(1, dev_name.as_bytes())?
.ok_or_else(|| StoreError::Invariant(format!("no such ublk device '{name}'")))?
.ino;
let inode = store
.get_inode(ino)?
.ok_or_else(|| StoreError::Invariant("device inode missing".into()))?;
Ok(Self {
store,
ino,
blocks: inode.size / BLOCK_SIZE,
})
}
pub fn capacity_bytes(&self) -> u64 {
self.blocks * BLOCK_SIZE
}
pub fn blocks(&self) -> u64 {
self.blocks
}
pub fn store(&self) -> &Store {
&self.store
}
pub fn store_mut(&mut self) -> &mut Store {
&mut self.store
}
pub fn read(&mut self, offset: u64, len: u64) -> Result<Vec<u8>, StoreError> {
if offset >= self.capacity_bytes() {
return Ok(Vec::new());
}
let end = (offset.saturating_add(len)).min(self.capacity_bytes());
self.store.read_file(self.ino, offset, end - offset)
}
pub fn write(&mut self, offset: u64, data: &[u8]) -> Result<u64, StoreError> {
if offset >= self.capacity_bytes() {
return Ok(0);
}
let room = self.capacity_bytes() - offset;
let n = (data.len() as u64).min(room);
self.store
.write_region(self.ino, offset, &data[..n as usize])?;
Ok(n)
}
pub fn flush(&mut self) -> Result<(), StoreError> {
self.store.durability_barrier(&CrashHooks::none())
}
pub fn discard(&mut self, offset: u64, len: u64) -> Result<(), StoreError> {
let end = (offset.saturating_add(len)).min(self.capacity_bytes());
if end > offset {
self.store.punch_hole(self.ino, offset, end, true)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn dev(dir: &TempDir) -> BlockStore {
BlockStore::open_or_create(
dir.path(),
&StoreConfig::default(),
"test0",
16 * 1024 * 1024,
)
.unwrap()
}
#[test]
fn block_roundtrip() {
let dir = TempDir::new().unwrap();
let mut d = dev(&dir);
assert_eq!(d.capacity_bytes(), 16 * 1024 * 1024);
let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
assert_eq!(d.write(0, &data).unwrap(), 4096);
d.flush().unwrap();
let read = d.read(0, 4096).unwrap();
assert_eq!(read, data);
let read2 = d.read(100, 100).unwrap();
assert_eq!(read2, &data[100..200]);
assert!(d.read(16 * 1024 * 1024 - 100, 4096).unwrap().len() <= 100);
assert_eq!(d.write(16 * 1024 * 1024 - 100, &data).unwrap(), 100);
}
#[test]
fn discard_reads_zeros_and_frees() {
let dir = TempDir::new().unwrap();
let mut d = dev(&dir);
let data = vec![0xABu8; 8192];
d.write(0, &data).unwrap();
d.flush().unwrap();
assert_eq!(d.read(0, 8192).unwrap(), data);
let used_before = d.store().physical_used();
d.discard(0, 8192).unwrap();
d.flush().unwrap();
let read = d.read(0, 8192).unwrap();
assert!(read.iter().all(|&b| b == 0), "discard must zero the range");
crate::store::gc::collect(d.store_mut(), &CrashHooks::none()).unwrap();
let used_after = d.store().physical_used();
assert!(used_after < used_before, "discard must free space after GC");
}
#[test]
fn device_survives_reopen_and_fsck() {
let dir = TempDir::new().unwrap();
let mut d = dev(&dir);
let data: Vec<u8> = (0..4096u32).map(|i| (i % 7) as u8).collect();
d.write(4096, &data).unwrap();
d.flush().unwrap();
drop(d);
let mut d2 = BlockStore::open(dir.path(), &StoreConfig::default(), "test0").unwrap();
assert_eq!(d2.read(4096, 4096).unwrap(), data);
let report = crate::fsck::fsck(dir.path(), &crate::fsck::FsckOptions::default()).unwrap();
assert!(report.is_clean(), "fsck: {}", report.render());
}
#[test]
fn device_is_visible_as_a_hidden_file() {
let dir = TempDir::new().unwrap();
let mut d = dev(&dir);
d.write(0, b"block-data").unwrap();
d.flush().unwrap();
drop(d);
let mut store = Store::open(dir.path(), &StoreConfig::default()).unwrap();
let entry = store
.dir_lookup(1, b".ublk-test0")
.unwrap()
.expect("device file");
assert!(entry.ino > 0);
store
.create_snapshot(b"block-snap", &CrashHooks::none())
.unwrap();
assert_eq!(store.list_snapshots().unwrap().len(), 1);
}
}