use crate::Result;
use crate::block::BlockDevice;
use super::mft;
pub const LOG_PAGE_SIZE: usize = 4096;
pub const RSTR_MAGIC: &[u8; 4] = b"RSTR";
pub const RCRD_MAGIC: &[u8; 4] = b"RCRD";
pub const LFS_MAJOR_VERSION: i16 = 1;
pub const LFS_MINOR_VERSION: i16 = 1;
pub const FLAG_SINGLE_PAGE_IO: u16 = 0x0001;
pub const FLAG_CLEAN_DISMOUNT: u16 = 0x0002;
pub const NO_CLIENT: u16 = 0xFFFF;
pub const RT_CLIENT_RECORD: u32 = 1;
pub const RT_CLIENT_RESTART: u32 = 2;
pub const PRIVATE_PAYLOAD_MAGIC: &[u8; 4] = b"FSTJ";
pub const SEQ_NUMBER_BITS: u32 = 50;
pub const SECTOR_SIZE: usize = 512;
pub fn lsn_to_file_offset(lsn: u64) -> u64 {
let shifted = lsn.wrapping_shl(SEQ_NUMBER_BITS);
shifted.wrapping_shr(SEQ_NUMBER_BITS - 3)
}
pub fn pack_lsn(seq: u64, file_offset: u64) -> u64 {
let off_bits = 64 - SEQ_NUMBER_BITS;
let off = (file_offset >> 3) & ((1u64 << off_bits) - 1);
(seq << off_bits) | off
}
pub fn lsn_seq(lsn: u64) -> u64 {
let off_bits = 64 - SEQ_NUMBER_BITS;
lsn >> off_bits
}
const _: () = ();
pub fn build_restart_page(
current_lsn: u64,
file_size: u64,
clean_dismount: bool,
) -> [u8; LOG_PAGE_SIZE] {
let mut page = [0u8; LOG_PAGE_SIZE];
page[0..4].copy_from_slice(RSTR_MAGIC);
let sectors = LOG_PAGE_SIZE / SECTOR_SIZE;
let usa_offset: u16 = 0x1E;
let usa_count: u16 = (sectors as u16) + 1;
page[4..6].copy_from_slice(&usa_offset.to_le_bytes());
page[6..8].copy_from_slice(&usa_count.to_le_bytes());
page[0x10..0x14].copy_from_slice(&(LOG_PAGE_SIZE as u32).to_le_bytes());
page[0x14..0x18].copy_from_slice(&(LOG_PAGE_SIZE as u32).to_le_bytes());
let restart_offset: u16 = 0x40; page[0x18..0x1A].copy_from_slice(&restart_offset.to_le_bytes());
page[0x1A..0x1C].copy_from_slice(&LFS_MINOR_VERSION.to_le_bytes());
page[0x1C..0x1E].copy_from_slice(&LFS_MAJOR_VERSION.to_le_bytes());
let ra = restart_offset as usize;
let mut flags = 0u16;
if clean_dismount {
flags |= FLAG_CLEAN_DISMOUNT;
}
page[ra..ra + 8].copy_from_slice(¤t_lsn.to_le_bytes());
page[ra + 8..ra + 10].copy_from_slice(&1u16.to_le_bytes());
page[ra + 10..ra + 12].copy_from_slice(&NO_CLIENT.to_le_bytes());
page[ra + 12..ra + 14].copy_from_slice(&0u16.to_le_bytes());
page[ra + 14..ra + 16].copy_from_slice(&flags.to_le_bytes());
page[ra + 16..ra + 20].copy_from_slice(&SEQ_NUMBER_BITS.to_le_bytes());
let restart_area_length: u16 = 0xA0;
page[ra + 20..ra + 22].copy_from_slice(&restart_area_length.to_le_bytes());
let client_array_offset: u16 = 0x30;
page[ra + 22..ra + 24].copy_from_slice(&client_array_offset.to_le_bytes());
page[ra + 24..ra + 32].copy_from_slice(&file_size.to_le_bytes());
page[ra + 36..ra + 38].copy_from_slice(&0x30u16.to_le_bytes());
page[ra + 38..ra + 40].copy_from_slice(&0x40u16.to_le_bytes());
page[ra + 40..ra + 44].copy_from_slice(&1u32.to_le_bytes());
let cr = ra + client_array_offset as usize;
page[cr + 8..cr + 16].copy_from_slice(¤t_lsn.to_le_bytes());
page[cr + 16..cr + 18].copy_from_slice(&NO_CLIENT.to_le_bytes());
page[cr + 18..cr + 20].copy_from_slice(&NO_CLIENT.to_le_bytes());
page[cr + 20..cr + 22].copy_from_slice(&1u16.to_le_bytes());
page[cr + 28..cr + 32].copy_from_slice(&8u32.to_le_bytes());
let name_utf16 = ['N', 'T', 'F', 'S'];
let mut p = cr + 32;
for c in name_utf16 {
let u = c as u16;
page[p..p + 2].copy_from_slice(&u.to_le_bytes());
p += 2;
}
mft::install_fixup(&mut page, SECTOR_SIZE, 1);
page
}
#[derive(Debug, Clone, Copy)]
pub struct RestartView {
pub current_lsn: u64,
pub client_restart_lsn: u64,
pub file_size: u64,
pub seq_number_bits: u32,
pub flags: u16,
}
impl RestartView {
pub fn is_clean(&self) -> bool {
self.flags & FLAG_CLEAN_DISMOUNT != 0
}
}
pub fn parse_restart_page(bytes: &[u8]) -> Option<RestartView> {
if bytes.len() < LOG_PAGE_SIZE {
return None;
}
let mut buf = vec![0u8; LOG_PAGE_SIZE];
buf.copy_from_slice(&bytes[..LOG_PAGE_SIZE]);
if &buf[0..4] != RSTR_MAGIC {
return None;
}
mft::apply_fixup(&mut buf, SECTOR_SIZE).ok()?;
let restart_offset = u16::from_le_bytes(buf[0x18..0x1A].try_into().unwrap()) as usize;
if restart_offset + 0x2C > buf.len() {
return None;
}
let ra = restart_offset;
let current_lsn = u64::from_le_bytes(buf[ra..ra + 8].try_into().unwrap());
let flags = u16::from_le_bytes(buf[ra + 14..ra + 16].try_into().unwrap());
let seq_number_bits = u32::from_le_bytes(buf[ra + 16..ra + 20].try_into().unwrap());
let file_size = u64::from_le_bytes(buf[ra + 24..ra + 32].try_into().unwrap());
let client_array_offset =
u16::from_le_bytes(buf[ra + 22..ra + 24].try_into().unwrap()) as usize;
let client_restart_lsn = if ra + client_array_offset + 16 <= buf.len() {
u64::from_le_bytes(
buf[ra + client_array_offset + 8..ra + client_array_offset + 16]
.try_into()
.unwrap(),
)
} else {
0
};
Some(RestartView {
current_lsn,
client_restart_lsn,
file_size,
seq_number_bits,
flags,
})
}
#[derive(Debug, Clone)]
pub struct RedoEntry {
pub target_offset: u64,
pub redo_bytes: Vec<u8>,
pub undo_bytes: Vec<u8>,
}
pub fn build_record_page(
records: &[RedoEntry],
first_lsn: u64,
) -> Result<([u8; LOG_PAGE_SIZE], u64)> {
let mut page = [0u8; LOG_PAGE_SIZE];
page[0..4].copy_from_slice(RCRD_MAGIC);
let sectors = LOG_PAGE_SIZE / SECTOR_SIZE;
let usa_offset: u16 = 0x28;
let usa_count: u16 = (sectors as u16) + 1;
page[4..6].copy_from_slice(&usa_offset.to_le_bytes());
page[6..8].copy_from_slice(&usa_count.to_le_bytes());
page[0x10..0x14].copy_from_slice(&1u32.to_le_bytes());
page[0x14..0x16].copy_from_slice(&1u16.to_le_bytes());
page[0x16..0x18].copy_from_slice(&1u16.to_le_bytes());
let mut cursor: usize = 0x40;
let mut last_lsn = first_lsn;
let mut last_end_lsn = first_lsn;
for (cur_lsn, r) in (first_lsn..).zip(records.iter()) {
let cd_len = 4 + 8 + 4 + r.redo_bytes.len() + r.undo_bytes.len();
let cd_padded = (cd_len + 7) & !7;
let needed = 0x30 + cd_padded;
if cursor + needed > LOG_PAGE_SIZE {
return Err(crate::Error::InvalidImage(
"ntfs: LFS record page would overflow — too many txn entries".into(),
));
}
page[cursor..cursor + 8].copy_from_slice(&cur_lsn.to_le_bytes());
page[cursor + 0x18..cursor + 0x1C].copy_from_slice(&(cd_len as u32).to_le_bytes());
page[cursor + 0x1C..cursor + 0x1E].copy_from_slice(&1u16.to_le_bytes());
page[cursor + 0x1E..cursor + 0x20].copy_from_slice(&0u16.to_le_bytes());
page[cursor + 0x20..cursor + 0x24].copy_from_slice(&RT_CLIENT_RECORD.to_le_bytes());
page[cursor + 0x24..cursor + 0x28].copy_from_slice(&1u32.to_le_bytes());
let cd = cursor + 0x30;
page[cd..cd + 4].copy_from_slice(PRIVATE_PAYLOAD_MAGIC);
page[cd + 4..cd + 12].copy_from_slice(&r.target_offset.to_le_bytes());
page[cd + 12..cd + 16].copy_from_slice(&(r.redo_bytes.len() as u32).to_le_bytes());
let mut p = cd + 16;
page[p..p + r.redo_bytes.len()].copy_from_slice(&r.redo_bytes);
p += r.redo_bytes.len();
page[p..p + r.undo_bytes.len()].copy_from_slice(&r.undo_bytes);
last_lsn = cur_lsn;
last_end_lsn = cur_lsn + 1;
cursor += needed;
}
page[0x08..0x10].copy_from_slice(&last_lsn.to_le_bytes());
page[0x18..0x1A].copy_from_slice(&(cursor as u16).to_le_bytes());
page[0x20..0x28].copy_from_slice(&last_end_lsn.to_le_bytes());
mft::install_fixup(&mut page, SECTOR_SIZE, 1);
Ok((page, last_lsn))
}
pub fn parse_record_page(bytes: &[u8]) -> Option<Vec<RedoEntry>> {
if bytes.len() < LOG_PAGE_SIZE {
return None;
}
let mut buf = vec![0u8; LOG_PAGE_SIZE];
buf.copy_from_slice(&bytes[..LOG_PAGE_SIZE]);
if &buf[0..4] != RCRD_MAGIC {
return None;
}
mft::apply_fixup(&mut buf, SECTOR_SIZE).ok()?;
let next_record_offset = u16::from_le_bytes(buf[0x18..0x1A].try_into().unwrap()) as usize;
if next_record_offset < 0x40 || next_record_offset > buf.len() {
return None;
}
let mut out = Vec::new();
let mut cursor = 0x40usize;
while cursor + 0x30 <= next_record_offset {
let cd_len =
u32::from_le_bytes(buf[cursor + 0x18..cursor + 0x1C].try_into().unwrap()) as usize;
if cd_len < 16 || cursor + 0x30 + cd_len > buf.len() {
break;
}
let cd = cursor + 0x30;
if &buf[cd..cd + 4] != PRIVATE_PAYLOAD_MAGIC {
let cd_padded = (cd_len + 7) & !7;
cursor += 0x30 + cd_padded;
continue;
}
let target_offset = u64::from_le_bytes(buf[cd + 4..cd + 12].try_into().unwrap());
let length = u32::from_le_bytes(buf[cd + 12..cd + 16].try_into().unwrap()) as usize;
if 16 + 2 * length > cd_len {
break;
}
let redo_start = cd + 16;
let undo_start = redo_start + length;
let redo_bytes = buf[redo_start..redo_start + length].to_vec();
let undo_bytes = buf[undo_start..undo_start + length].to_vec();
out.push(RedoEntry {
target_offset,
redo_bytes,
undo_bytes,
});
let cd_padded = (cd_len + 7) & !7;
cursor += 0x30 + cd_padded;
}
Some(out)
}
pub fn write_initial_logfile(
dev: &mut dyn BlockDevice,
logfile_offset: u64,
log_size: u64,
) -> Result<()> {
dev.zero_range(logfile_offset, log_size)?;
let page = build_restart_page(0, log_size, true);
dev.write_at(logfile_offset, &page)?;
dev.write_at(logfile_offset + LOG_PAGE_SIZE as u64, &page)?;
Ok(())
}
pub fn read_current_restart(
dev: &mut dyn BlockDevice,
logfile_offset: u64,
) -> Result<Option<(RestartView, u32)>> {
let mut buf0 = vec![0u8; LOG_PAGE_SIZE];
let mut buf1 = vec![0u8; LOG_PAGE_SIZE];
dev.read_at(logfile_offset, &mut buf0)?;
dev.read_at(logfile_offset + LOG_PAGE_SIZE as u64, &mut buf1)?;
let v0 = parse_restart_page(&buf0);
let v1 = parse_restart_page(&buf1);
let out = match (v0, v1) {
(Some(a), Some(b)) => {
if a.current_lsn >= b.current_lsn {
Some((a, 0u32))
} else {
Some((b, 1u32))
}
}
(Some(a), None) => Some((a, 0u32)),
(None, Some(b)) => Some((b, 1u32)),
(None, None) => None,
};
Ok(out)
}
pub fn walk_records(
dev: &mut dyn BlockDevice,
logfile_offset: u64,
log_size: u64,
) -> Result<Vec<RedoEntry>> {
let mut out = Vec::new();
let start = logfile_offset + 2 * LOG_PAGE_SIZE as u64;
let end = logfile_offset + log_size;
let mut p = start;
let mut buf = vec![0u8; LOG_PAGE_SIZE];
while p + LOG_PAGE_SIZE as u64 <= end {
dev.read_at(p, &mut buf)?;
if &buf[0..4] != RCRD_MAGIC {
break;
}
match parse_record_page(&buf) {
Some(entries) => out.extend(entries),
None => break,
}
p += LOG_PAGE_SIZE as u64;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restart_page_round_trip_clean() {
let page = build_restart_page(0xDEAD_BEEF, 64 * 1024, true);
let view = parse_restart_page(&page).expect("must parse");
assert!(view.is_clean());
assert_eq!(view.current_lsn, 0xDEAD_BEEF);
assert_eq!(view.file_size, 64 * 1024);
assert_eq!(view.seq_number_bits, SEQ_NUMBER_BITS);
}
#[test]
fn restart_page_round_trip_dirty() {
let page = build_restart_page(0xCAFE, 64 * 1024, false);
let view = parse_restart_page(&page).expect("must parse");
assert!(!view.is_clean());
assert_eq!(view.current_lsn, 0xCAFE);
}
#[test]
fn record_page_round_trip_single_entry() {
let entries = vec![RedoEntry {
target_offset: 4096,
redo_bytes: b"new bytes".to_vec(),
undo_bytes: b"old bytes".to_vec(),
}];
let (page, last_lsn) = build_record_page(&entries, 10).unwrap();
assert_eq!(last_lsn, 10);
let parsed = parse_record_page(&page).expect("must parse");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].target_offset, 4096);
assert_eq!(parsed[0].redo_bytes, b"new bytes");
assert_eq!(parsed[0].undo_bytes, b"old bytes");
}
#[test]
fn record_page_round_trip_multi_entry() {
let entries = vec![
RedoEntry {
target_offset: 100,
redo_bytes: vec![1, 2, 3],
undo_bytes: vec![4, 5, 6],
},
RedoEntry {
target_offset: 200,
redo_bytes: vec![7; 32],
undo_bytes: vec![8; 32],
},
];
let (page, last_lsn) = build_record_page(&entries, 100).unwrap();
assert_eq!(last_lsn, 101);
let parsed = parse_record_page(&page).expect("must parse");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].redo_bytes, vec![1, 2, 3]);
assert_eq!(parsed[1].undo_bytes, vec![8; 32]);
}
#[test]
fn lsn_packing_round_trip() {
let lsn = pack_lsn(7, 0x1000);
assert_eq!(lsn_seq(lsn), 7);
let off = lsn_to_file_offset(lsn);
assert_eq!(off, 0x1000);
}
#[test]
fn restart_page_rejects_zero_bytes() {
let buf = vec![0u8; LOG_PAGE_SIZE];
assert!(parse_restart_page(&buf).is_none());
}
#[test]
fn restart_page_rejects_short_buffer() {
let buf = vec![0u8; 16];
assert!(parse_restart_page(&buf).is_none());
}
}