use std::collections::{HashMap, HashSet};
use super::records::{parse_header, RecordHeader, REC_VER_CONTAINER};
const RT_CURRENT_USER_ATOM: u16 = 0x0FF6;
const RT_USER_EDIT_ATOM: u16 = 0x0FF5;
const RT_PERSIST_DIRECTORY: u16 = 0x1772;
const RT_DOCUMENT_CONTAINER: u16 = 0x03E8;
const RT_SLIDE_CONTAINER: u16 = 0x03EE;
const RT_SLIDE_LIST_WITH_TEXT: u16 = 0x0FF0;
const RT_SLIDE_PERSIST_ATOM: u16 = 0x03F3;
const TOKEN_PLAIN: u32 = 0xE391_C05F;
const MAX_EDITS: usize = 4096;
const MAX_DEPTH: usize = 32;
pub(super) enum LiveModel {
Persist {
document_body: (usize, usize),
slide_offsets: Vec<usize>,
},
Fallback,
}
fn u32_at(data: &[u8], at: usize) -> Option<u32> {
data.get(at..at + 4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn header_at(stream: &[u8], off: usize, want: u16) -> Option<RecordHeader> {
let (h, _) = parse_header(stream, off, stream.len())?;
(h.rec_type == want).then_some(h)
}
fn find_in(
stream: &[u8],
start: usize,
end: usize,
want: u16,
want_instance: Option<u16>,
depth: usize,
) -> Option<RecordHeader> {
if depth > MAX_DEPTH {
return None;
}
let mut pos = start;
while let Some((h, next)) = parse_header(stream, pos, end) {
if h.rec_type == want && want_instance.is_none_or(|i| h.rec_instance == i) {
return Some(h);
}
if h.rec_ver == REC_VER_CONTAINER {
if let Some(found) = find_in(
stream,
h.body_start,
h.body_end,
want,
want_instance,
depth + 1,
) {
return Some(found);
}
}
if next <= pos {
break;
}
pos = next;
}
None
}
pub(super) fn resolve(stream: &[u8], current_user: Option<&[u8]>) -> LiveModel {
match try_resolve(stream, current_user) {
Some(m) => m,
None => LiveModel::Fallback,
}
}
fn try_resolve(stream: &[u8], current_user: Option<&[u8]>) -> Option<LiveModel> {
let cu = current_user?;
let h = header_at(cu, 0, RT_CURRENT_USER_ATOM)?;
let token = u32_at(cu, h.body_start + 4)?;
if token != TOKEN_PLAIN {
return None;
}
let mut edit_off = u32_at(cu, h.body_start + 8)? as usize;
let mut persist: HashMap<u32, u32> = HashMap::new();
let mut visited: HashSet<usize> = HashSet::new();
let mut doc_persist_id: Option<u32> = None;
for _ in 0..MAX_EDITS {
if edit_off == 0 || !visited.insert(edit_off) {
break;
}
let eh = header_at(stream, edit_off, RT_USER_EDIT_ATOM)?;
let prev = u32_at(stream, eh.body_start + 8)? as usize;
let dir_off = u32_at(stream, eh.body_start + 12)? as usize;
if doc_persist_id.is_none() {
doc_persist_id = Some(u32_at(stream, eh.body_start + 16)?);
}
let dh = header_at(stream, dir_off, RT_PERSIST_DIRECTORY)?;
let mut pos = dh.body_start;
while pos + 4 <= dh.body_end {
let packed = u32_at(stream, pos)?;
let first_id = packed & 0x000F_FFFF;
let count = ((packed >> 20) & 0x0FFF) as usize;
pos += 4;
for i in 0..count {
if pos + 4 > dh.body_end {
break;
}
let off = u32_at(stream, pos)?;
persist.entry(first_id + i as u32).or_insert(off);
pos += 4;
}
}
edit_off = prev;
}
let doc_off = *persist.get(&doc_persist_id?)? as usize;
let dh = header_at(stream, doc_off, RT_DOCUMENT_CONTAINER)?;
let document_body = (dh.body_start, dh.body_end);
let mut slide_offsets = Vec::new();
if let Some(slwt) = find_in(
stream,
document_body.0,
document_body.1,
RT_SLIDE_LIST_WITH_TEXT,
Some(super::records::SLWT_INSTANCE_SLIDES),
0,
) {
let mut pos = slwt.body_start;
while let Some((ch, next)) = parse_header(stream, pos, slwt.body_end) {
if ch.rec_type == RT_SLIDE_PERSIST_ATOM {
if let Some(pid) = u32_at(stream, ch.body_start) {
if pid != 0 {
if let Some(&off) = persist.get(&pid) {
if header_at(stream, off as usize, RT_SLIDE_CONTAINER).is_some() {
slide_offsets.push(off as usize);
}
}
}
}
}
if next <= pos {
break;
}
pos = next;
}
}
Some(LiveModel::Persist {
document_body,
slide_offsets,
})
}