use alloc::boxed::Box;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU16, AtomicU64, Ordering};
use zeroize::Zeroizing;
use crate::{
jvck::{
codec::{JvckCbcCodec, MetadataCodec, ReplicaCtx, Unsealed},
metadata::{self, JvckHeader, JvckSecrets, METADATA_BLOCK_SIZE},
options::JvckMetadataOptions,
},
store::{EncryptedOffsetStore, SectorIo},
types::{EncryptedOffset, VolumeState},
VckError, VckResult,
};
#[derive(Debug, Clone, Copy)]
pub struct Geometry {
pub offset_sector: u64,
pub data_sectors: u64,
pub sector_size: u32,
}
pub struct JvckMetadataStore<S: SectorIo> {
io: S,
options: JvckMetadataOptions,
vmk: Zeroizing<Vec<u8>>,
geometry: Geometry,
volume_sectors: u64,
header: JvckHeader,
secrets: JvckSecrets,
offset: AtomicU64,
state: AtomicU16,
codec: Box<dyn MetadataCodec>,
}
fn replica_sectors(metadata_size: u32, sector_size: u32) -> u64 {
(metadata_size / sector_size) as u64
}
fn metadata_sector_lbas(
volume_sectors: u64,
replica_sectors: u64,
use_header: u32,
use_footer: u32,
) -> Vec<u64> {
let mut lbas = Vec::with_capacity((use_header + use_footer) as usize);
for i in 0..use_header as u64 {
lbas.push(i * replica_sectors);
}
let footer_start = volume_sectors - use_footer as u64 * replica_sectors;
for j in 0..use_footer as u64 {
let region_start = footer_start + j * replica_sectors;
lbas.push(region_start + replica_sectors - 1);
}
lbas
}
fn vendor_data_base_lba_at(
volume_sectors: u64,
rs: u64,
use_header: u32,
use_footer: u32,
replica_index: usize,
) -> Option<u64> {
let uh = use_header as usize;
let uf = use_footer as usize;
if replica_index < uh {
Some(replica_index as u64 * rs + 1)
} else if replica_index < uh + uf {
let j = (replica_index - uh) as u64;
let footer_start = volume_sectors - uf as u64 * rs;
Some(footer_start + j * rs)
} else {
None
}
}
fn compute_geometry(
sector_size: u32,
volume_sectors: u64,
options: &JvckMetadataOptions,
) -> VckResult<Geometry> {
if (sector_size as usize) < METADATA_BLOCK_SIZE {
return Err(VckError::Unsupported("sector size smaller than 512"));
}
let rs = replica_sectors(options.metadata_size, sector_size);
if rs == 0 {
return Err(VckError::ValidationFailed(
"metadata_size smaller than one sector",
));
}
let consumed = (options.use_header + options.use_footer) as u64 * rs;
if consumed >= volume_sectors {
return Err(VckError::ValidationFailed(
"volume too small to hold metadata replicas",
));
}
Ok(Geometry {
offset_sector: options.use_header as u64 * rs,
data_sectors: volume_sectors - consumed,
sector_size,
})
}
fn read_block<S: SectorIo>(
io: &S,
sector_size: u32,
lba: u64,
) -> VckResult<[u8; METADATA_BLOCK_SIZE]> {
let mut sector = alloc::vec![0u8; sector_size as usize];
io.read_sectors(lba, &mut sector)?;
let mut block = [0u8; METADATA_BLOCK_SIZE];
block.copy_from_slice(§or[..METADATA_BLOCK_SIZE]);
Ok(block)
}
fn write_block<S: SectorIo>(
io: &S,
sector_size: u32,
lba: u64,
block: &[u8; METADATA_BLOCK_SIZE],
) -> VckResult<()> {
let mut sector = alloc::vec![0u8; sector_size as usize];
sector[..METADATA_BLOCK_SIZE].copy_from_slice(block);
io.write_sectors(lba, §or)
}
impl<S: SectorIo> JvckMetadataStore<S> {
pub fn open(io: S, vmk: &[u8]) -> VckResult<Self> {
JvckMetadataReader::open(io)?.into_store(vmk, |ctx| {
let codec: Box<dyn MetadataCodec> = Box::new(JvckCbcCodec);
let unsealed = codec.unseal(ctx, vmk)?;
Ok((codec, unsealed))
})
}
pub fn create(
io: S,
vmk: &[u8],
options: JvckMetadataOptions,
fvek_key1: [u8; 32],
fvek_key2: [u8; 32],
volume_id: [u8; 16],
codec: Box<dyn MetadataCodec>,
) -> VckResult<Self> {
options.validate()?;
let sector_size = io.sector_size();
let volume_sectors = io.total_sectors();
let geometry = compute_geometry(sector_size, volume_sectors, &options)?;
let header = JvckHeader {
vendor_id: 0,
metadata_version: 1,
vendor_version: 0,
metadata_size: options.metadata_size,
sector_size,
header_replica_count: options.use_header as u8,
footer_replica_count: options.use_footer as u8,
volume_id,
vendor_reserved: [0u8; metadata::VENDOR_RESERVED_SIZE],
};
let secrets = JvckSecrets {
fvek_key1,
fvek_key2,
};
let store = Self {
io,
options,
vmk: Zeroizing::new(vmk.to_vec()),
geometry,
volume_sectors,
header,
secrets,
offset: AtomicU64::new(0),
state: AtomicU16::new(VolumeState::Encrypt.as_u16()),
codec,
};
store.write_all_replicas()?;
Ok(store)
}
pub fn load_offset(&self) -> VckResult<u64> {
let sector_size = self.geometry.sector_size;
let rs = replica_sectors(self.options.metadata_size, sector_size);
let lbas = metadata_sector_lbas(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
);
let mut best: Option<u64> = None;
for (idx, lba) in lbas.iter().enumerate() {
let block = match read_block(&self.io, sector_size, *lba) {
Ok(block) => block,
Err(_) => continue,
};
if metadata::verify_crc(&block).is_err() {
continue;
}
let vendor_base = vendor_data_base_lba_at(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
idx,
)
.unwrap_or(0);
let ctx = ReplicaCtx::new(
&self.header,
block,
&self.io as &dyn SectorIo,
vendor_base,
rs.saturating_sub(1),
sector_size,
idx,
);
if let Ok(offset) = self.codec.read_offset(&ctx, &self.vmk) {
best = Some(best.map_or(offset, |b| b.max(offset)));
}
}
best.ok_or(VckError::NotFound("no valid JVCK metadata replica"))
}
pub fn fvek_keys(&self) -> (&[u8; 32], &[u8; 32]) {
(&self.secrets.fvek_key1, &self.secrets.fvek_key2)
}
pub fn volume_id(&self) -> [u8; 16] {
self.header.volume_id
}
fn write_all_replicas(&self) -> VckResult<()> {
let mut salt = [0u8; metadata::SALT_SIZE];
crate::rng::fill_random(&mut salt)?;
let encrypted_offset = self.offset.load(Ordering::Relaxed);
let state = VolumeState::from_u16(self.state.load(Ordering::Relaxed));
let mut block = [0u8; METADATA_BLOCK_SIZE];
self.codec.seal(
&self.header,
&self.secrets,
encrypted_offset,
state,
&salt,
&self.vmk,
&mut block,
)?;
let rs = replica_sectors(self.options.metadata_size, self.geometry.sector_size);
for lba in metadata_sector_lbas(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
) {
write_block(&self.io, self.geometry.sector_size, lba, &block)?;
}
Ok(())
}
pub fn offset_sector(&self) -> u64 {
self.geometry.offset_sector
}
pub fn data_sector_count(&self) -> u64 {
self.geometry.data_sectors
}
pub fn sector_size(&self) -> u32 {
self.geometry.sector_size
}
pub fn footer_replica_count(&self) -> u32 {
self.options.use_footer
}
pub fn metadata_size(&self) -> u32 {
self.options.metadata_size
}
pub fn header(&self) -> &JvckHeader {
&self.header
}
pub fn replica_count(&self) -> usize {
(self.options.use_header + self.options.use_footer) as usize
}
pub fn vendor_data_sector_count(&self) -> u64 {
replica_sectors(self.options.metadata_size, self.geometry.sector_size).saturating_sub(1)
}
fn vendor_data_base_lba(&self, replica_index: usize) -> Option<u64> {
let rs = replica_sectors(self.options.metadata_size, self.geometry.sector_size);
vendor_data_base_lba_at(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
replica_index,
)
}
fn vendor_data_lba_checked(
&self,
replica_index: usize,
rel_sector: u64,
len: usize,
) -> VckResult<u64> {
let ss = self.geometry.sector_size as usize;
if ss == 0 || len == 0 || !len.is_multiple_of(ss) {
return Err(VckError::InvalidData(
"vendor data buffer must be a non-zero multiple of the sector size",
));
}
let nsec = (len / ss) as u64;
let base = self
.vendor_data_base_lba(replica_index)
.ok_or(VckError::NotFound("vendor data replica index out of range"))?;
let count = self.vendor_data_sector_count();
if rel_sector.checked_add(nsec).is_none_or(|end| end > count) {
return Err(VckError::ValidationFailed(
"vendor data range exceeds the replica region",
));
}
Ok(base + rel_sector)
}
pub fn read_vendor_data(
&self,
replica_index: usize,
rel_sector: u64,
buf: &mut [u8],
) -> VckResult<()> {
let lba = self.vendor_data_lba_checked(replica_index, rel_sector, buf.len())?;
self.io.read_sectors(lba, buf)
}
pub fn write_vendor_data(
&self,
replica_index: usize,
rel_sector: u64,
buf: &[u8],
) -> VckResult<()> {
let lba = self.vendor_data_lba_checked(replica_index, rel_sector, buf.len())?;
self.io.write_sectors(lba, buf)
}
pub fn write_vendor_data_all(&self, rel_sector: u64, buf: &[u8]) -> VckResult<()> {
self.vendor_data_lba_checked(0, rel_sector, buf.len())?;
for replica_index in 0..self.replica_count() {
self.write_vendor_data(replica_index, rel_sector, buf)?;
}
Ok(())
}
pub fn vendor_reserved(&self) -> &[u8; metadata::VENDOR_RESERVED_SIZE] {
&self.header.vendor_reserved
}
pub fn set_vendor_reserved(
&mut self,
vendor_reserved: &[u8; metadata::VENDOR_RESERVED_SIZE],
) -> VckResult<()> {
self.header.vendor_reserved = *vendor_reserved;
self.write_all_replicas()
}
}
impl<S: SectorIo> EncryptedOffsetStore for JvckMetadataStore<S>
where
S: Send + Sync + 'static,
{
fn load(&self) -> VckResult<EncryptedOffset> {
Ok(EncryptedOffset {
sector: self.load_offset()?,
total_sectors: self.geometry.data_sectors,
})
}
fn store(&self, offset: &EncryptedOffset) -> VckResult<()> {
self.offset.store(offset.sector, Ordering::Relaxed);
self.write_all_replicas()
}
fn flush(&self) -> VckResult<()> {
Ok(())
}
fn load_state(&self) -> VckResult<VolumeState> {
Ok(VolumeState::from_u16(self.state.load(Ordering::Relaxed)))
}
fn store_state(&self, state: VolumeState) -> VckResult<()> {
self.state.store(state.as_u16(), Ordering::Relaxed);
self.write_all_replicas()
}
}
pub struct JvckMetadataReader<S: SectorIo> {
io: S,
options: JvckMetadataOptions,
geometry: Geometry,
volume_sectors: u64,
header: JvckHeader,
}
impl<S: SectorIo> JvckMetadataReader<S> {
pub fn open(io: S) -> VckResult<Self> {
let sector_size = io.sector_size();
if (sector_size as usize) < METADATA_BLOCK_SIZE {
return Err(VckError::Unsupported("sector size smaller than 512"));
}
let volume_sectors = io.total_sectors();
if volume_sectors == 0 {
return Err(VckError::NotFound("empty volume"));
}
for lba in [volume_sectors - 1, 0] {
let block = read_block(&io, sector_size, lba)?;
if metadata::verify_crc(&block).is_err() {
continue;
}
let header = JvckHeader::parse(&block)?;
let options = JvckMetadataOptions {
use_header: header.header_replica_count as u32,
use_footer: header.footer_replica_count as u32,
metadata_size: header.metadata_size,
};
let geometry = compute_geometry(sector_size, volume_sectors, &options)?;
return Ok(Self {
io,
options,
geometry,
volume_sectors,
header,
});
}
Err(VckError::NotFound("no JVCK metadata present"))
}
pub fn header(&self) -> &JvckHeader {
&self.header
}
pub fn geometry(&self) -> Geometry {
self.geometry
}
pub fn replica_count(&self) -> usize {
(self.options.use_header + self.options.use_footer) as usize
}
pub fn replica_ctx(&self, replica_index: usize) -> VckResult<ReplicaCtx<'_>> {
let sector_size = self.geometry.sector_size;
let rs = replica_sectors(self.options.metadata_size, sector_size);
let lbas = metadata_sector_lbas(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
);
let lba = *lbas
.get(replica_index)
.ok_or(VckError::NotFound("replica index out of range"))?;
let block = read_block(&self.io, sector_size, lba)?;
metadata::verify_crc(&block)?;
let vendor_base = vendor_data_base_lba_at(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
replica_index,
)
.unwrap_or(0);
Ok(ReplicaCtx::new(
&self.header,
block,
&self.io as &dyn SectorIo,
vendor_base,
rs.saturating_sub(1),
sector_size,
replica_index,
))
}
pub fn read_vendor_data(
&self,
replica_index: usize,
rel_sector: u64,
buf: &mut [u8],
) -> VckResult<()> {
let sector_size = self.geometry.sector_size;
let ss = sector_size as usize;
if ss == 0 || buf.is_empty() || !buf.len().is_multiple_of(ss) {
return Err(VckError::InvalidData(
"vendor data buffer must be a non-zero multiple of the sector size",
));
}
let rs = replica_sectors(self.options.metadata_size, sector_size);
let base = vendor_data_base_lba_at(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
replica_index,
)
.ok_or(VckError::NotFound("vendor data replica index out of range"))?;
let nsec = (buf.len() / ss) as u64;
if rel_sector
.checked_add(nsec)
.is_none_or(|end| end > rs.saturating_sub(1))
{
return Err(VckError::ValidationFailed(
"vendor data range exceeds the replica region",
));
}
self.io.read_sectors(base + rel_sector, buf)
}
pub fn into_store<F>(self, vmk: &[u8], mut select: F) -> VckResult<JvckMetadataStore<S>>
where
F: FnMut(&ReplicaCtx<'_>) -> VckResult<(Box<dyn MetadataCodec>, Unsealed)>,
{
let sector_size = self.geometry.sector_size;
let rs = replica_sectors(self.options.metadata_size, sector_size);
let lbas = metadata_sector_lbas(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
);
let mut chosen: Option<(Box<dyn MetadataCodec>, Unsealed)> = None;
let mut last_err: Option<VckError> = None;
for (idx, lba) in lbas.iter().enumerate() {
let block = match read_block(&self.io, sector_size, *lba) {
Ok(b) => b,
Err(e) => {
last_err = Some(e);
continue;
}
};
if metadata::verify_crc(&block).is_err() {
continue;
}
let vendor_base = vendor_data_base_lba_at(
self.volume_sectors,
rs,
self.options.use_header,
self.options.use_footer,
idx,
)
.unwrap_or(0);
let ctx = ReplicaCtx::new(
&self.header,
block,
&self.io as &dyn SectorIo,
vendor_base,
rs.saturating_sub(1),
sector_size,
idx,
);
match select(&ctx) {
Ok(pair) => {
chosen = Some(pair);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
let (codec, unsealed) = chosen.ok_or_else(|| {
last_err.unwrap_or(VckError::NotFound(
"no JVCK metadata replica could be unsealed",
))
})?;
let store = JvckMetadataStore {
io: self.io,
options: self.options,
vmk: Zeroizing::new(vmk.to_vec()),
geometry: self.geometry,
volume_sectors: self.volume_sectors,
header: self.header,
secrets: unsealed.secrets,
offset: AtomicU64::new(0),
state: AtomicU16::new(unsealed.state.as_u16()),
codec,
};
let recovered = store.load_offset().unwrap_or(unsealed.encrypted_offset);
store.offset.store(recovered, Ordering::Relaxed);
Ok(store)
}
}
#[cfg(feature = "uefi")]
pub use uefi_io::{locate_block_io_volume, open_volume_footer_uefi, UefiBlockIoVolume};
#[cfg(feature = "uefi")]
mod uefi_io {
use super::*;
use crate::types::{guid_from_windows_bytes, Guid};
use alloc::format;
use uefi::boot::{self, open_protocol_exclusive, SearchType};
use uefi::proto::media::block::BlockIO;
use uefi::proto::media::partition::PartitionInfo;
pub struct UefiBlockIoVolume {
block_io: uefi::boot::ScopedProtocol<BlockIO>,
media_id: u32,
sector_size: u32,
total_sectors: u64,
}
unsafe impl Send for UefiBlockIoVolume {}
unsafe impl Sync for UefiBlockIoVolume {}
impl SectorIo for UefiBlockIoVolume {
fn sector_size(&self) -> u32 {
self.sector_size
}
fn total_sectors(&self) -> u64 {
self.total_sectors
}
fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> VckResult<()> {
self.block_io
.read_blocks(self.media_id, lba, buf)
.map_err(|e| VckError::Io(format!("BlockIO.ReadBlocks(lba={lba}) failed: {e:?}")))
}
fn write_sectors(&self, _lba: u64, _buf: &[u8]) -> VckResult<()> {
Err(VckError::Unsupported("loader Block IO volume is read-only"))
}
}
pub fn locate_block_io_volume(partition_guid: Guid) -> VckResult<UefiBlockIoVolume> {
let handles = boot::locate_handle_buffer(SearchType::from_proto::<BlockIO>())
.map_err(|e| VckError::Io(format!("locate BlockIO handles failed: {e:?}")))?;
for &handle in handles.iter() {
let matched = match open_protocol_exclusive::<PartitionInfo>(handle) {
Ok(pinfo) => match pinfo.gpt_partition_entry() {
Some(gpt) => {
guid_from_windows_bytes(gpt.unique_partition_guid.to_bytes())
== partition_guid
}
None => false,
},
Err(_) => false,
};
if !matched {
continue;
}
let block_io = open_protocol_exclusive::<BlockIO>(handle)
.map_err(|e| VckError::Io(format!("open BlockIO failed: {e:?}")))?;
let media = block_io.media();
if !media.is_media_present() {
return Err(VckError::Io(
"matched partition has no media present".into(),
));
}
let sector_size = media.block_size();
let media_id = media.media_id();
let total_sectors = media.last_block().saturating_add(1);
return Ok(UefiBlockIoVolume {
block_io,
media_id,
sector_size,
total_sectors,
});
}
Err(VckError::NotFound(
"no Block IO partition matched the target GUID",
))
}
pub fn open_volume_footer_uefi(
partition_guid: Guid,
vmk: &[u8],
) -> VckResult<JvckMetadataStore<UefiBlockIoVolume>> {
let io = locate_block_io_volume(partition_guid)?;
JvckMetadataStore::open(io, vmk)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jvck::codec::default_codec;
use std::sync::Mutex;
struct TestRng;
impl crate::rng::RandomSource for TestRng {
fn fill(&self, buf: &mut [u8]) -> VckResult<()> {
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(7).wrapping_add(1);
}
Ok(())
}
}
static TEST_RNG: TestRng = TestRng;
fn ensure_rng() {
crate::rng::set_random_source(&TEST_RNG);
}
struct MemVolume {
sector_size: u32,
data: Mutex<Vec<u8>>,
}
impl MemVolume {
fn new(sector_size: u32, sectors: u64) -> Self {
Self {
sector_size,
data: Mutex::new(alloc::vec![0u8; (sectors * sector_size as u64) as usize]),
}
}
}
impl SectorIo for MemVolume {
fn sector_size(&self) -> u32 {
self.sector_size
}
fn total_sectors(&self) -> u64 {
self.data.lock().unwrap().len() as u64 / self.sector_size as u64
}
fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> VckResult<()> {
let data = self.data.lock().unwrap();
let start = (lba * self.sector_size as u64) as usize;
buf.copy_from_slice(&data[start..start + buf.len()]);
Ok(())
}
fn write_sectors(&self, lba: u64, buf: &[u8]) -> VckResult<()> {
let mut data = self.data.lock().unwrap();
let start = (lba * self.sector_size as u64) as usize;
data[start..start + buf.len()].copy_from_slice(buf);
Ok(())
}
}
const VMK: &[u8] = b"unit-test-volume-master-key";
const MD_SIZE: u32 = 128 * 1024;
fn footer_only_options() -> JvckMetadataOptions {
JvckMetadataOptions {
use_header: 0,
use_footer: 2,
metadata_size: MD_SIZE,
}
}
#[test]
fn create_then_load_geometry() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
assert_eq!(store.offset_sector(), 0);
assert_eq!(store.data_sector_count(), 512);
assert_eq!(store.footer_replica_count(), 2);
assert_eq!(store.load_offset().unwrap(), 0);
assert_eq!(store.fvek_keys().0, &[1u8; 32]);
assert_eq!(store.volume_id(), [9; 16]);
}
#[test]
fn header_plus_footer_geometry() {
ensure_rng();
let io = MemVolume::new(512, 1280);
let opts = JvckMetadataOptions {
use_header: 1,
use_footer: 2,
metadata_size: MD_SIZE,
};
let store =
JvckMetadataStore::create(io, VMK, opts, [3; 32], [4; 32], [7; 16], default_codec())
.unwrap();
assert_eq!(store.offset_sector(), 256);
assert_eq!(store.data_sector_count(), 512);
}
#[test]
fn store_then_load_offset_roundtrip() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
store
.store(&EncryptedOffset {
sector: 1234,
total_sectors: 512,
})
.unwrap();
let loaded = store.load().unwrap();
assert_eq!(loaded.sector, 1234);
assert_eq!(loaded.total_sectors, 512);
}
#[test]
fn reopen_finds_existing_metadata() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[5; 32],
[6; 32],
[8; 16],
default_codec(),
)
.unwrap();
store
.store(&EncryptedOffset {
sector: 777,
total_sectors: 512,
})
.unwrap();
let io = store.io;
let reopened = JvckMetadataStore::open(io, VMK).unwrap();
assert_eq!(reopened.offset_sector(), 0);
assert_eq!(reopened.data_sector_count(), 512);
assert_eq!(reopened.load_offset().unwrap(), 777);
}
#[test]
fn recovery_picks_largest_offset() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
store
.store(&EncryptedOffset {
sector: 500,
total_sectors: 512,
})
.unwrap();
let mut block = [0u8; METADATA_BLOCK_SIZE];
store
.header
.encode(
&store.secrets,
300,
VolumeState::Encrypt,
&[0u8; metadata::SALT_SIZE],
VMK,
&mut block,
)
.unwrap();
write_block(&store.io, 512, store.volume_sectors - 1, &block).unwrap();
assert_eq!(store.load_offset().unwrap(), 500);
}
#[test]
fn state_persists_across_reopen() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
assert_eq!(store.load_state().unwrap(), VolumeState::Encrypt);
store
.store(&EncryptedOffset {
sector: 100,
total_sectors: 512,
})
.unwrap();
store.store_state(VolumeState::Decrypt).unwrap();
let io = store.io;
let reopened = JvckMetadataStore::open(io, VMK).unwrap();
assert_eq!(reopened.load_state().unwrap(), VolumeState::Decrypt);
assert_eq!(reopened.load_offset().unwrap(), 100);
}
#[test]
fn vendor_data_read_write_roundtrip() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
assert_eq!(store.replica_count(), 2);
assert_eq!(store.vendor_data_sector_count(), 255);
let data = alloc::vec![0xCDu8; 512];
store.write_vendor_data(0, 3, &data).unwrap();
let mut back = alloc::vec![0u8; 512];
store.read_vendor_data(0, 3, &mut back).unwrap();
assert_eq!(back, data);
assert!(store.write_vendor_data(0, 255, &data).is_err());
assert!(store.write_vendor_data(2, 0, &data).is_err());
assert!(store.read_vendor_data(0, 0, &mut [0u8; 100]).is_err());
assert_eq!(store.load_offset().unwrap(), 0);
}
#[test]
fn write_vendor_data_all_mirrors_every_replica() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
assert_eq!(store.replica_count(), 2);
let data = alloc::vec![0x5Au8; 1024]; store.write_vendor_data_all(7, &data).unwrap();
for replica in 0..store.replica_count() {
let mut back = alloc::vec![0u8; 1024];
store.read_vendor_data(replica, 7, &mut back).unwrap();
assert_eq!(back, data, "replica {replica} vendor data mismatch");
}
assert!(store.write_vendor_data_all(255, &data).is_err());
assert_eq!(store.load_offset().unwrap(), 0);
}
#[test]
fn set_vendor_reserved_persists_to_all_replicas() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let mut store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
assert_eq!(
store.vendor_reserved(),
&[0u8; metadata::VENDOR_RESERVED_SIZE]
);
let vr = [0xABu8; metadata::VENDOR_RESERVED_SIZE];
store.set_vendor_reserved(&vr).unwrap();
assert_eq!(store.vendor_reserved(), &vr);
let io = store.io;
let reader = JvckMetadataReader::open(io).unwrap();
assert_eq!(reader.header().vendor_reserved, vr);
for replica in 0..reader.replica_count() {
let ctx = reader.replica_ctx(replica).unwrap();
assert_eq!(ctx.header().vendor_reserved, vr, "replica {replica}");
}
}
#[test]
fn reader_replica_ctx_exposes_block_and_vendor_data() {
ensure_rng();
let io = MemVolume::new(512, 1024);
let store = JvckMetadataStore::create(
io,
VMK,
footer_only_options(),
[1; 32],
[2; 32],
[9; 16],
default_codec(),
)
.unwrap();
let marker = alloc::vec![0xE7u8; 512];
store.write_vendor_data_all(0, &marker).unwrap();
let io = store.io;
let reader = JvckMetadataReader::open(io).unwrap();
assert_eq!(reader.replica_count(), 2);
let ctx = reader.replica_ctx(1).unwrap();
assert_eq!(ctx.replica_index(), 1);
assert_eq!(&ctx.block()[..4], b"JVCK");
assert_eq!(
ctx.encrypted_metadata().len(),
metadata::ENCRYPTED_METADATA_SIZE
);
let mut vd = alloc::vec![0u8; 512];
ctx.read_vendor_data(0, &mut vd).unwrap();
assert_eq!(vd, marker);
assert!(reader.replica_ctx(2).is_err());
}
#[test]
fn open_empty_volume_fails() {
let io = MemVolume::new(512, 1024);
assert!(matches!(
JvckMetadataStore::open(io, VMK),
Err(VckError::NotFound(_))
));
}
#[test]
fn metadata_size_not_multiple_of_sector_is_floored() {
ensure_rng();
let sector_size = 4096u32;
let md_size = 128 * 1024 + 100;
let opts = JvckMetadataOptions {
use_header: 0,
use_footer: 2,
metadata_size: md_size,
};
let expected_rs = (md_size / sector_size) as u64; assert_eq!(expected_rs, 32);
let io = MemVolume::new(sector_size, 128);
let store =
JvckMetadataStore::create(io, VMK, opts, [1; 32], [2; 32], [9; 16], default_codec())
.unwrap();
assert_eq!(store.data_sector_count(), 128 - 2 * expected_rs);
assert_eq!(store.sector_size(), sector_size);
store
.store(&EncryptedOffset {
sector: 7,
total_sectors: store.data_sector_count(),
})
.unwrap();
let reopened = JvckMetadataStore::open(store.io, VMK).unwrap();
assert_eq!(reopened.metadata_size(), md_size);
assert_eq!(reopened.load_offset().unwrap(), 7);
}
}