use std::collections::{HashMap, HashSet};
use super::records::{
RT_CURRENT_USER_ATOM, RT_PERSIST_DIRECTORY_ATOM, RT_USER_EDIT_ATOM, RecordHeader, RecordIter,
};
const MAX_EDIT_CHAIN_LEN: usize = 4096;
#[derive(Debug, Default, Clone)]
pub struct PersistDirectory {
offsets: HashMap<u32, u32>,
pub doc_persist_id: u32,
}
impl PersistDirectory {
pub fn resolve(&self, persist_id: u32) -> Option<usize> {
self.offsets.get(&persist_id).map(|&o| o as usize)
}
}
pub fn build(stream: &[u8], current_user: Option<&[u8]>) -> Option<PersistDirectory> {
let start = current_user
.and_then(offset_to_current_edit)
.filter(|&off| user_edit_atom_at(stream, off).is_some())
.or_else(|| find_latest_user_edit_atom(stream))?;
let mut dir = PersistDirectory::default();
let mut offset = Some(start);
let mut seen = HashSet::new();
let mut doc_persist_id = None;
for _ in 0..MAX_EDIT_CHAIN_LEN {
let Some(off) = offset else { break };
if !seen.insert(off) {
break; }
let Some(edit) = user_edit_atom_at(stream, off) else {
break;
};
if doc_persist_id.is_none() {
doc_persist_id = Some(edit.doc_persist_id_ref);
}
if let Some(entries) = persist_directory_entries_at(stream, edit.offset_persist_directory) {
for (id, off) in entries {
dir.offsets.entry(id).or_insert(off);
}
}
offset = if edit.offset_last_edit == 0 {
None
} else {
Some(edit.offset_last_edit as usize)
};
}
dir.doc_persist_id = doc_persist_id.unwrap_or(1);
Some(dir)
}
struct UserEditAtom {
offset_last_edit: u32,
offset_persist_directory: u32,
doc_persist_id_ref: u32,
}
fn user_edit_atom_at(stream: &[u8], offset: usize) -> Option<UserEditAtom> {
let header_bytes = stream.get(offset..offset + 8)?;
let header = RecordHeader::parse(header_bytes).ok()?;
if header.rec_type != RT_USER_EDIT_ATOM || header.is_container() {
return None;
}
let body_start = offset + 8;
let body_end = body_start
.saturating_add(header.rec_len as usize)
.min(stream.len());
let body = stream.get(body_start..body_end)?;
if body.len() < 20 {
return None;
}
Some(UserEditAtom {
offset_last_edit: u32::from_le_bytes([body[8], body[9], body[10], body[11]]),
offset_persist_directory: u32::from_le_bytes([body[12], body[13], body[14], body[15]]),
doc_persist_id_ref: u32::from_le_bytes([body[16], body[17], body[18], body[19]]),
})
}
fn offset_to_current_edit(current_user: &[u8]) -> Option<usize> {
let header = RecordHeader::parse(current_user.get(0..8)?).ok()?;
if header.rec_type != RT_CURRENT_USER_ATOM || header.is_container() {
return None;
}
let body = current_user.get(8..)?;
let field = body.get(8..12)?;
Some(u32::from_le_bytes([field[0], field[1], field[2], field[3]]) as usize)
}
fn persist_directory_entries_at(stream: &[u8], offset: u32) -> Option<Vec<(u32, u32)>> {
let offset = offset as usize;
let header_bytes = stream.get(offset..offset + 8)?;
let header = RecordHeader::parse(header_bytes).ok()?;
if header.rec_type != RT_PERSIST_DIRECTORY_ATOM || header.is_container() {
return None;
}
let body_start = offset + 8;
let body_end = body_start
.saturating_add(header.rec_len as usize)
.min(stream.len());
let body = stream.get(body_start..body_end)?;
let mut entries = Vec::new();
let mut pos = 0usize;
while pos + 4 <= body.len() {
let word = u32::from_le_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
let persist_id = word & 0x000F_FFFF;
let c_persist = (word >> 20) as usize;
pos += 4;
for i in 0..c_persist {
let Some(chunk) = body.get(pos..pos + 4) else {
break;
};
let off = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
entries.push((persist_id.wrapping_add(i as u32), off));
pos += 4;
}
}
Some(entries)
}
fn find_latest_user_edit_atom(stream: &[u8]) -> Option<usize> {
let mut latest = None;
for rec in RecordIter::new(stream) {
let Ok(rec) = rec else { break };
if rec.header.rec_type == RT_USER_EDIT_ATOM && !rec.header.is_container() {
latest = Some(rec.offset);
}
}
latest
}
#[cfg(test)]
mod tests {
use super::*;
fn make_atom(rec_type: u16, instance: u16, data: &[u8]) -> Vec<u8> {
let ver_instance: u16 = instance << 4;
let mut buf = Vec::new();
buf.extend_from_slice(&ver_instance.to_le_bytes());
buf.extend_from_slice(&rec_type.to_le_bytes());
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
buf.extend_from_slice(data);
buf
}
fn user_edit_atom_bytes(
offset_last_edit: u32,
offset_persist_directory: u32,
doc_persist_id_ref: u32,
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&offset_last_edit.to_le_bytes());
body.extend_from_slice(&offset_persist_directory.to_le_bytes());
body.extend_from_slice(&doc_persist_id_ref.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); make_atom(RT_USER_EDIT_ATOM, 0, &body)
}
fn persist_directory_bytes(entries: &[(u32, u32)]) -> Vec<u8> {
assert!(!entries.is_empty());
let persist_id = entries[0].0;
let c_persist = entries.len() as u32;
let header = persist_id | (c_persist << 20);
let mut body = header.to_le_bytes().to_vec();
for (_, off) in entries {
body.extend_from_slice(&off.to_le_bytes());
}
make_atom(RT_PERSIST_DIRECTORY_ATOM, 0, &body)
}
fn current_user_bytes(offset_to_current_edit: u32) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&offset_to_current_edit.to_le_bytes());
body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u16.to_le_bytes()); body.push(0); body.push(0); body.extend_from_slice(&0u16.to_le_bytes()); make_atom(RT_CURRENT_USER_ATOM, 0, &body)
}
#[test]
fn persist_directory_entry_bit_layout() {
let pd = persist_directory_bytes(&[(5, 100), (6, 200), (7, 300)]);
let mut stream = vec![0u8; 16];
stream.extend(pd);
let entries = persist_directory_entries_at(&stream, 16).unwrap();
assert_eq!(entries, vec![(5, 100), (6, 200), (7, 300)]);
}
#[test]
fn resolves_current_edit_via_current_user_stream() {
let mut stream = Vec::new();
let pd_offset = stream.len() as u32;
stream.extend(persist_directory_bytes(&[(1, 0), (2, 42)]));
let edit_offset = stream.len() as u32;
stream.extend(user_edit_atom_bytes(0, pd_offset, 1));
let current_user = current_user_bytes(edit_offset);
let dir = build(&stream, Some(¤t_user)).unwrap();
assert_eq!(dir.doc_persist_id, 1);
assert_eq!(dir.resolve(1), Some(0));
assert_eq!(dir.resolve(2), Some(42));
}
#[test]
fn falls_back_to_brute_force_scan_without_current_user_stream() {
let mut stream = Vec::new();
let pd_offset = stream.len() as u32;
stream.extend(persist_directory_bytes(&[(1, 0), (2, 42)]));
stream.extend(user_edit_atom_bytes(0, pd_offset, 1));
let dir = build(&stream, None).unwrap();
assert_eq!(dir.resolve(2), Some(42));
}
#[test]
fn newer_edit_entry_wins_over_older_edit_for_same_persist_id() {
let mut stream = Vec::new();
let pd1_offset = stream.len() as u32;
stream.extend(persist_directory_bytes(&[(2, 10)]));
let edit1_offset = stream.len() as u32;
stream.extend(user_edit_atom_bytes(0, pd1_offset, 1));
let pd2_offset = stream.len() as u32;
stream.extend(persist_directory_bytes(&[(2, 99)]));
let edit2_offset = stream.len() as u32;
stream.extend(user_edit_atom_bytes(edit1_offset, pd2_offset, 1));
let current_user = current_user_bytes(edit2_offset);
let dir = build(&stream, Some(¤t_user)).unwrap();
assert_eq!(dir.resolve(2), Some(99), "newest edit's directory entry must win");
}
#[test]
fn cyclic_offset_last_edit_does_not_hang() {
let mut stream = Vec::new();
let pd_offset = stream.len() as u32;
stream.extend(persist_directory_bytes(&[(1, 0)]));
let edit_offset = stream.len() as u32;
stream.extend(user_edit_atom_bytes(edit_offset, pd_offset, 1));
let current_user = current_user_bytes(edit_offset);
let dir = build(&stream, Some(¤t_user)).unwrap();
assert_eq!(dir.resolve(1), Some(0));
}
#[test]
fn no_user_edit_atom_returns_none() {
assert!(build(&[], None).is_none());
assert!(build(b"not a ppt stream at all", None).is_none());
}
}