use crate::{Result, compression, reader::RevitFile, streams::GLOBAL_LATEST};
use encoding_rs::UTF_16LE;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentHistory {
pub entries: Vec<String>,
pub string_section_offset: usize,
}
impl DocumentHistory {
pub fn from_revit_file(rf: &mut RevitFile) -> Result<Self> {
let bytes = rf.read_stream(GLOBAL_LATEST)?;
let (_, decompressed) = compression::inflate_stream_auto(GLOBAL_LATEST, &bytes)?;
Self::from_decompressed(&decompressed)
}
pub fn from_decompressed(decomp: &[u8]) -> Result<Self> {
const PROBE: [u8; 12] = [b'R', 0, b'e', 0, b'v', 0, b'i', 0, b't', 0, b' ', 0];
let mut entries = Vec::new();
let first = decomp
.windows(PROBE.len())
.position(|w| w == PROBE)
.ok_or_else(|| {
crate::Error::Decompress(
"no 'Revit ' UTF-16LE marker found in decompressed Global/Latest".into(),
)
})?;
let string_section_offset = first.saturating_sub(8);
let mut scan = 0usize;
while scan + PROBE.len() <= decomp.len() {
let idx = match decomp[scan..].windows(PROBE.len()).position(|w| w == PROBE) {
Some(p) => scan + p,
None => break,
};
let mut end = idx;
let max_end = (idx + 512).min(decomp.len());
while end + 2 <= max_end {
let c = u16::from_le_bytes([decomp[end], decomp[end + 1]]);
if c == 0 {
break;
}
if c < 0x20 && c != b' ' as u16 {
break;
}
end += 2;
}
if end > idx {
let (text, _, _) = UTF_16LE.decode(&decomp[idx..end]);
let s = text.trim_end_matches(&['\0', '/', ' '] as &[_]).to_string();
if s.starts_with("Revit ") && !entries.contains(&s) {
entries.push(s);
}
}
scan = end.max(idx + 1);
}
Ok(Self {
entries,
string_section_offset,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StringRecord {
pub offset: usize,
pub tag: u32,
pub value: String,
}
pub fn extract_string_records(decomp: &[u8]) -> Vec<StringRecord> {
let mut out = Vec::new();
let mut candidates = RecordCandidates::default();
let mut i = 0usize;
while i + 8 < decomp.len() {
let Some(next) = candidates.next(decomp, i) else {
break;
};
i = next;
let tag = u32::from_le_bytes([decomp[i], decomp[i + 1], decomp[i + 2], decomp[i + 3]]);
let cnt = u32::from_le_bytes([decomp[i + 4], decomp[i + 5], decomp[i + 6], decomp[i + 7]])
as usize;
if (1..=400).contains(&cnt) && i + 8 + 2 * cnt <= decomp.len() {
let body = &decomp[i + 8..i + 8 + 2 * cnt];
if is_printable_ascii_utf16le(body) {
let (cow, _, _) = UTF_16LE.decode(body);
let text = cow.into_owned();
out.push(StringRecord {
offset: i,
tag,
value: text
.trim_end_matches(&['\0', '/', ' ', '\t'] as &[_])
.to_string(),
});
i += 8 + 2 * cnt;
continue;
}
}
i += 1;
}
out
}
#[derive(Default)]
struct RecordCandidates {
start: Option<usize>,
hits: u64,
}
impl RecordCandidates {
fn next(&mut self, buf: &[u8], from: usize) -> Option<usize> {
const LOW7: u64 = 0x7f7f_7f7f_7f7f_7f7f;
let zero_bits = |word: u64| -> u128 {
let high = !((word & LOW7).wrapping_add(LOW7) | word | LOW7);
u128::from((high >> 7).wrapping_mul(0x0102_0408_1020_4080) >> 56)
};
let mut at = from;
loop {
if let Some(start) = self
.start
.filter(|start| (*start..*start + 64).contains(&at))
{
let left = self.hits >> (at - start);
if left != 0 {
return Some(at + left.trailing_zeros() as usize);
}
at = start + 64;
continue;
}
if at + 5 + 72 > buf.len() {
break;
}
let (mut zeros, mut small) = (0u128, 0u128);
for word in 0..9 {
let first = at + 5 + 8 * word;
let bytes: [u8; 8] = buf[first..first + 8].try_into().expect("8 bytes");
let value = u64::from_le_bytes(bytes);
zeros |= zero_bits(value) << (8 * word);
small |= zero_bits(value & !0x0101_0101_0101_0101) << (8 * word);
}
self.start = Some(at);
self.hits = (small & (zeros >> 1) & (zeros >> 2) & !(zeros >> 3) & (zeros >> 4)) as u64;
}
while at + 9 < buf.len() {
if buf[at + 5] <= 1
&& buf[at + 6] == 0
&& buf[at + 7] == 0
&& buf[at + 8] != 0
&& buf[at + 9] == 0
{
return Some(at);
}
at += 1;
}
None
}
}
fn is_printable_ascii_utf16le(body: &[u8]) -> bool {
!body.is_empty()
&& body.chunks_exact(2).all(|unit| {
unit[1] == 0
&& (unit[0].is_ascii_graphic()
|| unit[0] == b' '
|| unit[0] == b'\t'
|| unit[0] == b'\n')
})
}
pub fn string_records_from_file(rf: &mut crate::RevitFile) -> Result<Vec<StringRecord>> {
let bytes = rf.read_stream(GLOBAL_LATEST)?;
let (_, decomp) = compression::inflate_stream_auto(GLOBAL_LATEST, &bytes)?;
Ok(extract_string_records(&decomp))
}
pub fn string_records_from_partitions(rf: &mut crate::RevitFile) -> Result<Vec<StringRecord>> {
Ok(rf.partition_string_records()?.as_ref().clone())
}
pub(crate) fn extract_partition_string_records(
rf: &mut crate::RevitFile,
) -> Result<Vec<StringRecord>> {
let partition_name = rf
.partition_stream_name()
.ok_or_else(|| crate::Error::StreamNotFound("no Partitions/NN stream".into()))?;
let inflated = rf.inflated_partition(&partition_name)?;
let sep = [0xFFu8; 16];
let mut joined =
Vec::with_capacity(inflated.bytes().len() + sep.len() * inflated.chunk_count());
for (i, c) in inflated.chunks().enumerate() {
if i > 0 {
joined.extend_from_slice(&sep);
}
joined.extend_from_slice(c);
}
Ok(extract_string_records(&joined))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_empty_input() {
let res = DocumentHistory::from_decompressed(&[]);
assert!(res.is_err(), "empty input should fail");
}
#[test]
fn parses_contrived_single_entry() {
let version_str = "Revit 2024 20230308_1635(x64)";
let utf16: Vec<u8> = version_str
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let mut buf = Vec::new();
buf.extend(std::iter::repeat_n(0u8, 64)); buf.extend_from_slice(&7u32.to_le_bytes()); buf.extend_from_slice(&(version_str.chars().count() as u32).to_le_bytes()); buf.extend_from_slice(&utf16);
buf.extend_from_slice(&[0u8, 0u8]);
let history = DocumentHistory::from_decompressed(&buf).unwrap();
assert_eq!(history.entries.len(), 1);
assert!(
history.entries[0].starts_with("Revit 2024"),
"got: {:?}",
history.entries[0]
);
}
#[test]
fn parses_multiple_entries() {
fn push_entry(buf: &mut Vec<u8>, version: &str) {
let utf16: Vec<u8> = version
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
buf.extend_from_slice(&7u32.to_le_bytes());
buf.extend_from_slice(&(version.chars().count() as u32).to_le_bytes());
buf.extend_from_slice(&utf16);
buf.extend_from_slice(&[0u8, 0u8]);
buf.extend_from_slice(&[0xff, 0xff, 0xab, 0xcd]);
}
let mut buf = Vec::new();
buf.extend(std::iter::repeat_n(0u8, 64));
push_entry(&mut buf, "Revit 2018 (Build X)");
push_entry(&mut buf, "Revit 2024 (Build Y)");
let history = DocumentHistory::from_decompressed(&buf).unwrap();
assert_eq!(history.entries.len(), 2);
assert!(history.entries[0].starts_with("Revit 2018"));
assert!(history.entries[1].starts_with("Revit 2024"));
}
}