1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Q6 probe: parse the Global/Latest class-tag directory and test
//! whether the payload u16/u32 values make sense as:
//! (a) file-offset pointers into Global/Latest where instance data lives
//! (b) instance counts
//! (c) tag-relative offsets into a separate Partitions chunk
//!
//! Strategy: enumerate directory entries, then look at what the payload
//! value lands on if interpreted as a file offset into the decompressed
//! Global/Latest stream.
#![allow(
clippy::needless_range_loop,
clippy::type_complexity,
clippy::collapsible_if,
clippy::collapsible_match
)]
use rvt::{RevitFile, compression, streams::GLOBAL_LATEST};
fn main() -> anyhow::Result<()> {
let path = std::env::args()
.nth(1)
.expect("usage: directory_probe <file>");
let mut rf = RevitFile::open(&path)?;
let raw = rf.read_stream(GLOBAL_LATEST)?;
let d = compression::inflate_at(&raw, 8)?;
println!("Global/Latest: {} bytes decompressed", d.len());
// Document upgrade history lives at the start of the stream.
// The class-tag directory begins AFTER it. Scan for the first
// u32 LE value that looks like a small tag (0x0000-0x4000 range)
// preceded by something that looks like a u16 payload (< 0x4000).
// Empirically (from record_framing.rs output) the directory begins
// around offset 0x430 in the 2024 file; we'll scan from 0x400 to
// find it robustly.
let scan_start = 0x400;
let scan_end = (scan_start + 0x2000).min(d.len());
#[derive(Debug)]
struct DirEntry {
offset: usize,
tag: u32,
payload_u16: u16,
entry_span: usize,
}
let mut entries: Vec<DirEntry> = Vec::new();
// Walk forward. An entry is `[u32 tag][variable bytes until next u32 tag]`.
let mut i = scan_start;
while i + 8 < scan_end {
let tag = u32::from_le_bytes([d[i], d[i + 1], d[i + 2], d[i + 3]]);
if tag == 0 || tag > 0x4000 {
i += 1;
continue;
}
// Find the next candidate tag (u32 in same range)
let mut next_at = None;
let mut j = i + 4;
while j + 4 <= scan_end {
let t2 = u32::from_le_bytes([d[j], d[j + 1], d[j + 2], d[j + 3]]);
if t2 > tag && t2 <= tag + 20 && t2 < 0x4000 {
next_at = Some(j);
break;
}
j += 2;
if j - i > 40 {
break;
}
}
let span = next_at.map(|n| n - i).unwrap_or(6);
// Payload: the first 2 bytes after the tag
let payload = if i + 6 <= scan_end {
u16::from_le_bytes([d[i + 4], d[i + 5]])
} else {
0
};
entries.push(DirEntry {
offset: i,
tag,
payload_u16: payload,
entry_span: span,
});
i += span.max(6);
}
println!("\nDirectory entries found: {}", entries.len());
println!("First 30 (sequential tags ascending):");
for e in entries.iter().take(30) {
print!(
" @0x{:04x} tag=0x{:04x} ({:4}) payload u16={:5} (0x{:04x}) span={}",
e.offset, e.tag, e.tag, e.payload_u16, e.payload_u16, e.entry_span
);
// Interpret payload as offset: is d[payload..payload+16] structured?
let off = e.payload_u16 as usize;
if off > 0 && off + 8 < d.len() {
let a = u32::from_le_bytes([d[off], d[off + 1], d[off + 2], d[off + 3]]);
let b = u32::from_le_bytes([d[off + 4], d[off + 5], d[off + 6], d[off + 7]]);
print!(" | payload-as-offset[{off}]: u32={a} u32={b}");
}
println!();
}
// Check monotonicity: if payload is an offset, it should be monotonic
// (or at least all-in-range).
let mut in_range_as_offset = 0;
let mut beyond_stream = 0;
for e in &entries {
let off = e.payload_u16 as usize;
if off < d.len() {
in_range_as_offset += 1;
} else {
beyond_stream += 1;
}
}
println!(
"\nPayload-as-offset sanity: {} in range, {} beyond stream end ({} bytes)",
in_range_as_offset,
beyond_stream,
d.len()
);
// Alt interpretation: payload is a COUNT of instances for this class
// elsewhere. Total instances across all directory entries:
let total: u32 = entries.iter().map(|e| e.payload_u16 as u32).sum();
println!(
"Payload-as-count sanity: sum across all {} entries = {}",
entries.len(),
total
);
Ok(())
}