use crate::Result;
use crate::block::BlockDevice;
use super::writer::{JOURNAL_HEADER_ENDIAN, JOURNAL_HEADER_MAGIC, VOL_ATTR_JOURNALED};
pub const JOURNAL_SECTOR: u64 = 512;
pub const BLHDR_SIZE: u32 = 4096;
pub const JHDR_SIZE: u32 = 512;
const BINFO_SIZE: usize = 16;
const BLHDR_FIXED_SIZE: usize = 16;
#[derive(Debug, Clone)]
pub(crate) struct PendingBlock {
pub dev_off: u64,
pub data: Vec<u8>,
}
pub(crate) enum FlushSink<'d> {
Direct(&'d mut dyn BlockDevice),
Buffered(Vec<PendingBlock>),
}
impl<'d> FlushSink<'d> {
pub fn write_at(&mut self, dev_off: u64, data: &[u8]) -> Result<()> {
match self {
FlushSink::Direct(dev) => dev.write_at(dev_off, data),
FlushSink::Buffered(blocks) => {
for slot in blocks.iter_mut() {
if slot.dev_off == dev_off && slot.data.len() == data.len() {
slot.data.clear();
slot.data.extend_from_slice(data);
return Ok(());
}
}
blocks.push(PendingBlock {
dev_off,
data: data.to_vec(),
});
Ok(())
}
}
}
}
pub(crate) struct JournalLog {
pub buf_off: u64,
pub buf_size: u64,
pub start: u64,
pub end: u64,
pending: Vec<PendingBlock>,
}
impl JournalLog {
pub fn load(
dev: &mut dyn BlockDevice,
vh: &super::volume_header::VolumeHeader,
) -> Result<Option<Self>> {
if vh.attributes & VOL_ATTR_JOURNALED == 0 {
return Ok(None);
}
let info_block = vh.journal_info_block;
if info_block == 0 {
return Ok(None);
}
let bs = u64::from(vh.block_size);
let info_off = u64::from(info_block) * bs;
let mut info = [0u8; 52];
dev.read_at(info_off, &mut info)?;
let buf_off = u64::from_be_bytes(info[36..44].try_into().unwrap());
let buf_size = u64::from_be_bytes(info[44..52].try_into().unwrap());
if buf_off == 0 || buf_size == 0 {
return Ok(None);
}
let mut hdr = [0u8; 24];
dev.read_at(buf_off, &mut hdr)?;
let magic = u32::from_be_bytes(hdr[0..4].try_into().unwrap());
let endian = u32::from_be_bytes(hdr[4..8].try_into().unwrap());
if magic != JOURNAL_HEADER_MAGIC || endian != JOURNAL_HEADER_ENDIAN {
return Err(crate::Error::InvalidImage(format!(
"hfs+ journal: unrecognised header magic/endian \
({magic:#010x}/{endian:#010x})"
)));
}
let start = u64::from_be_bytes(hdr[8..16].try_into().unwrap());
let end = u64::from_be_bytes(hdr[16..24].try_into().unwrap());
Ok(Some(Self {
buf_off,
buf_size,
start,
end,
pending: Vec::new(),
}))
}
pub fn is_dirty(&self) -> bool {
self.start != self.end
}
pub fn add(&mut self, dev_off: u64, data: Vec<u8>) {
for slot in self.pending.iter_mut() {
if slot.dev_off == dev_off && slot.data.len() == data.len() {
slot.data = data;
return;
}
}
self.pending.push(PendingBlock { dev_off, data });
}
pub fn add_batch(&mut self, blocks: Vec<PendingBlock>) {
for b in blocks {
self.add(b.dev_off, b.data);
}
}
pub fn lookup(&self, dev_off: u64) -> Option<(u64, &[u8])> {
for p in self.pending.iter().rev() {
let end = p.dev_off + p.data.len() as u64;
if dev_off >= p.dev_off && dev_off < end {
return Some((p.dev_off, &p.data));
}
}
None
}
pub fn commit(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.pending.is_empty() {
return Ok(());
}
let entries = std::mem::take(&mut self.pending);
let mut padded: Vec<(u64, Vec<u8>)> = Vec::with_capacity(entries.len());
for e in entries {
let pad_len = (e.data.len() as u64).div_ceil(JOURNAL_SECTOR) * JOURNAL_SECTOR;
let mut buf = e.data;
buf.resize(pad_len as usize, 0);
padded.push((e.dev_off, buf));
}
let num_info: u16 = u16::try_from(padded.len() + 1).map_err(|_| {
crate::Error::Unsupported(
"hfs+ journal: too many blocks in one transaction (>65534)".into(),
)
})?;
let data_total: u32 = padded
.iter()
.map(|(_, d)| d.len() as u32)
.fold(0u32, |a, b| a.saturating_add(b));
let bytes_used: u32 = BLHDR_SIZE.checked_add(data_total).ok_or_else(|| {
crate::Error::Unsupported("hfs+ journal: transaction overflows u32".into())
})?;
if u64::from(bytes_used) > self.buf_size - u64::from(JHDR_SIZE) {
return Err(crate::Error::Unsupported(
"hfs+ journal: transaction larger than journal buffer".into(),
));
}
let mut tx = vec![0u8; bytes_used as usize];
tx[0..2].copy_from_slice(&num_info.to_be_bytes());
tx[2..4].copy_from_slice(&num_info.to_be_bytes());
tx[4..8].copy_from_slice(&bytes_used.to_be_bytes());
let info_base = BLHDR_FIXED_SIZE;
tx[info_base..info_base + 8].copy_from_slice(&u64::from(bytes_used).to_be_bytes());
for (i, (dev_off, data)) in padded.iter().enumerate() {
let slot = info_base + (i + 1) * BINFO_SIZE;
let sector = dev_off / JOURNAL_SECTOR;
let bsize = data.len() as u32;
tx[slot..slot + 8].copy_from_slice(§or.to_be_bytes());
tx[slot + 8..slot + 12].copy_from_slice(&bsize.to_be_bytes());
}
let mut cursor = BLHDR_SIZE as usize;
for (_off, data) in &padded {
tx[cursor..cursor + data.len()].copy_from_slice(data);
cursor += data.len();
}
let csum = crc32_reflected(&tx[..BLHDR_FIXED_SIZE]);
tx[8..12].copy_from_slice(&csum.to_be_bytes());
let usable = self.buf_size - u64::from(JHDR_SIZE);
let tx_off_in_ring = self.end - u64::from(JHDR_SIZE);
let new_end;
if tx_off_in_ring + u64::from(bytes_used) > usable {
new_end = u64::from(JHDR_SIZE) + u64::from(bytes_used);
self.write_at_buffer(dev, u64::from(JHDR_SIZE), &tx)?;
} else {
new_end = self.end + u64::from(bytes_used);
self.write_at_buffer(dev, tx_off_in_ring + u64::from(JHDR_SIZE), &tx)?;
}
write_journal_header(dev, self.buf_off, self.start, new_end, self.buf_size)?;
dev.sync()?;
self.end = new_end;
for (dev_off, data) in &padded {
dev.write_at(*dev_off, data)?;
}
dev.sync()?;
write_journal_header(dev, self.buf_off, self.end, self.end, self.buf_size)?;
dev.sync()?;
self.start = self.end;
Ok(())
}
fn write_at_buffer(&self, dev: &mut dyn BlockDevice, ring_off: u64, data: &[u8]) -> Result<()> {
let abs = self.buf_off + ring_off;
if abs + data.len() as u64 > self.buf_off + self.buf_size {
return Err(crate::Error::Unsupported(
"hfs+ journal: write would overflow buffer".into(),
));
}
dev.write_at(abs, data)
}
}
pub(crate) fn replay(
dev: &mut dyn BlockDevice,
vh: &super::volume_header::VolumeHeader,
) -> Result<()> {
let Some(log) = JournalLog::load(dev, vh)? else {
return Ok(());
};
if !log.is_dirty() {
return Ok(());
}
let mut cursor = log.start;
let end = log.end;
while cursor != end {
let mut hdr_fixed = [0u8; BLHDR_FIXED_SIZE];
dev.read_at(log.buf_off + cursor, &mut hdr_fixed)?;
let num_blocks = u16::from_be_bytes(hdr_fixed[2..4].try_into().unwrap()) as usize;
let bytes_used = u32::from_be_bytes(hdr_fixed[4..8].try_into().unwrap());
if num_blocks == 0 || bytes_used < BLHDR_SIZE {
return Err(crate::Error::InvalidImage(format!(
"hfs+ journal: malformed block list (num={num_blocks}, bytes={bytes_used})"
)));
}
let info_bytes = num_blocks * BINFO_SIZE;
let mut info = vec![0u8; info_bytes];
dev.read_at(log.buf_off + cursor + BLHDR_FIXED_SIZE as u64, &mut info)?;
let mut data_cursor = log.buf_off + cursor + u64::from(BLHDR_SIZE);
for i in 1..num_blocks {
let slot = i * BINFO_SIZE;
let sector = u64::from_be_bytes(info[slot..slot + 8].try_into().unwrap());
let bsize = u32::from_be_bytes(info[slot + 8..slot + 12].try_into().unwrap()) as usize;
let mut data = vec![0u8; bsize];
dev.read_at(data_cursor, &mut data)?;
let target = sector * JOURNAL_SECTOR;
dev.write_at(target, &data)?;
data_cursor += bsize as u64;
}
cursor += u64::from(bytes_used);
if cursor >= log.buf_size {
cursor = u64::from(JHDR_SIZE);
}
}
write_journal_header(dev, log.buf_off, end, end, log.buf_size)?;
dev.sync()?;
Ok(())
}
pub(crate) fn write_journal_header(
dev: &mut dyn BlockDevice,
buf_off: u64,
start: u64,
end: u64,
buf_size: u64,
) -> Result<()> {
let mut b = [0u8; JHDR_SIZE as usize];
b[0..4].copy_from_slice(&JOURNAL_HEADER_MAGIC.to_be_bytes());
b[4..8].copy_from_slice(&JOURNAL_HEADER_ENDIAN.to_be_bytes());
b[8..16].copy_from_slice(&start.to_be_bytes());
b[16..24].copy_from_slice(&end.to_be_bytes());
b[24..32].copy_from_slice(&buf_size.to_be_bytes());
b[32..36].copy_from_slice(&BLHDR_SIZE.to_be_bytes());
b[36..40].copy_from_slice(&0u32.to_be_bytes());
b[40..44].copy_from_slice(&JHDR_SIZE.to_be_bytes());
let csum = crc32_reflected(&b);
b[36..40].copy_from_slice(&csum.to_be_bytes());
dev.write_at(buf_off, &b)?;
Ok(())
}
fn crc32_reflected(buf: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in buf {
let mut c = (crc ^ u32::from(byte)) & 0xff;
for _ in 0..8 {
c = if c & 1 != 0 {
(c >> 1) ^ 0xEDB8_8320
} else {
c >> 1
};
}
crc = (crc >> 8) ^ c;
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn journal_commit_writes_data_and_advances_start() {
let mut dev = MemoryBackend::new(64 * 1024);
let buf_off: u64 = 4096;
let buf_size: u64 = 16 * 1024;
write_journal_header(
&mut dev,
buf_off,
u64::from(JHDR_SIZE),
u64::from(JHDR_SIZE),
buf_size,
)
.unwrap();
let mut log = JournalLog {
buf_off,
buf_size,
start: u64::from(JHDR_SIZE),
end: u64::from(JHDR_SIZE),
pending: Vec::new(),
};
log.add(32 * 1024, vec![0xAB; 512]);
log.commit(&mut dev).unwrap();
let mut got = [0u8; 512];
dev.read_at(32 * 1024, &mut got).unwrap();
assert!(got.iter().all(|&b| b == 0xAB));
assert_eq!(log.start, log.end);
}
#[test]
fn replay_reapplies_pending_transaction() {
let mut dev = MemoryBackend::new(128 * 1024);
let buf_off: u64 = 8192;
let buf_size: u64 = 16 * 1024;
write_journal_header(
&mut dev,
buf_off,
u64::from(JHDR_SIZE),
u64::from(JHDR_SIZE),
buf_size,
)
.unwrap();
let mut log = JournalLog {
buf_off,
buf_size,
start: u64::from(JHDR_SIZE),
end: u64::from(JHDR_SIZE),
pending: Vec::new(),
};
log.add(32 * 1024, vec![0xCD; 512]);
log.commit(&mut dev).unwrap();
dev.write_at(32 * 1024, &[0u8; 512]).unwrap();
write_journal_header(&mut dev, buf_off, u64::from(JHDR_SIZE), log.end, buf_size).unwrap();
use crate::fs::hfs_plus::volume_header::{
ExtentDescriptor, FORK_EXTENT_COUNT, ForkData, VolumeHeader,
};
let blank_fork = ForkData {
logical_size: 0,
clump_size: 0,
total_blocks: 0,
extents: [ExtentDescriptor::default(); FORK_EXTENT_COUNT],
};
let vh = VolumeHeader {
signature: *b"H+",
version: 4,
attributes: VOL_ATTR_JOURNALED,
journal_info_block: 1, block_size: 4096,
total_blocks: 16,
free_blocks: 0,
next_catalog_id: 16,
allocation_file: blank_fork,
extents_file: blank_fork,
catalog_file: blank_fork,
attributes_file: blank_fork,
startup_file: blank_fork,
};
let info_off = u64::from(vh.journal_info_block) * u64::from(vh.block_size);
let mut info = [0u8; 52];
info[0..4].copy_from_slice(&2u32.to_be_bytes());
info[36..44].copy_from_slice(&buf_off.to_be_bytes());
info[44..52].copy_from_slice(&buf_size.to_be_bytes());
dev.write_at(info_off, &info).unwrap();
replay(&mut dev, &vh).unwrap();
let mut got = [0u8; 512];
dev.read_at(32 * 1024, &mut got).unwrap();
assert!(got.iter().all(|&b| b == 0xCD), "replay restored data");
}
}