use clap::Parser;
use rvt::compression;
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(
name = "gen-fixture",
version,
about = "Synthesize a minimal Revit-file-alike CFB fixture for tests",
after_help = "Examples:\n \
gen-fixture demo --classes Wall,Level,Door --element-count 25 --output demo.rvt"
)]
struct Cli {
name: String,
#[arg(short = 'o', long = "output")]
output: Option<PathBuf>,
#[arg(long = "seed", default_value_t = 0u64)]
seed: u64,
#[arg(long = "classes", default_value = "Wall,Level,Project")]
classes: String,
#[arg(long = "element-count", default_value_t = 10usize)]
element_count: usize,
#[arg(long = "year", default_value_t = 2024u32)]
year: u32,
}
fn main() -> ExitCode {
rvt::cli::exit_quietly_on_broken_pipe();
let cli = Cli::parse();
match run(cli) {
Ok(path) => {
eprintln!("wrote {}", path.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}
fn run(cli: Cli) -> anyhow::Result<PathBuf> {
let classes: Vec<String> = cli
.classes
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
for c in &classes {
if !is_valid_class_name(c.as_bytes()) {
anyhow::bail!(
"invalid class name {c:?}: must start uppercase ASCII and \
contain only [A-Za-z0-9_]"
);
}
}
if classes.is_empty() {
anyhow::bail!("--classes must name at least one class");
}
let out_path = resolve_output_path(&cli)?;
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let spec = FixtureSpec {
name: cli.name.clone(),
seed: cli.seed,
classes,
element_count: cli.element_count,
year: cli.year,
};
write_fixture(&out_path, &spec)?;
Ok(out_path)
}
fn resolve_output_path(cli: &Cli) -> anyhow::Result<PathBuf> {
if let Some(p) = &cli.output {
return Ok(p.clone());
}
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap());
Ok(manifest_dir
.join("tests")
.join("fixtures")
.join(format!("synthetic-{}.rvt", cli.name)))
}
struct FixtureSpec {
name: String,
seed: u64,
classes: Vec<String>,
element_count: usize,
year: u32,
}
fn write_fixture(path: &std::path::Path, spec: &FixtureSpec) -> anyhow::Result<()> {
let basic_file_info = build_basic_file_info(spec);
let formats_latest = build_formats_latest(spec)?;
let global_latest = build_global_latest(spec)?;
let revit_preview = build_revit_preview();
let part_atom = build_part_atom();
let partition_nn = rvt::streams::partition_for_year(spec.year).unwrap_or(67);
let partitions_stream = format!("Partitions/{partition_nn}");
let streams: Vec<(&str, Vec<u8>)> = vec![
(rvt::streams::BASIC_FILE_INFO, basic_file_info),
(rvt::streams::CONTENTS, vec![]),
(rvt::streams::FORMATS_LATEST, formats_latest),
(rvt::streams::GLOBAL_CONTENT_DOCUMENTS, vec![]),
(rvt::streams::GLOBAL_DOC_INCREMENT_TABLE, vec![]),
(rvt::streams::GLOBAL_ELEM_TABLE, vec![]),
(rvt::streams::GLOBAL_HISTORY, vec![]),
(rvt::streams::GLOBAL_LATEST, global_latest),
(rvt::streams::GLOBAL_PARTITION_TABLE, vec![]),
(rvt::streams::PART_ATOM, part_atom),
(rvt::streams::REVIT_PREVIEW_4_0, revit_preview),
(rvt::streams::TRANSMISSION_DATA, vec![]),
(&partitions_stream, vec![]),
];
let out_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
let mut out =
cfb::CompoundFile::create(out_file).map_err(|e| anyhow::anyhow!("cfb create: {e}"))?;
let mut created: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for (name, _) in &streams {
let norm = format!("/{}", name.trim_start_matches('/'));
let parts: Vec<&str> = norm.split('/').filter(|s| !s.is_empty()).collect();
for n in 1..parts.len() {
let parent = format!("/{}", parts[..n].join("/"));
if created.insert(parent.clone()) {
out.create_storage(&parent)
.map_err(|e| anyhow::anyhow!("create_storage {parent}: {e}"))?;
}
}
}
for (name, data) in streams {
let path = format!("/{}", name.trim_start_matches('/'));
let mut s = out
.create_stream(&path)
.map_err(|e| anyhow::anyhow!("create_stream {path}: {e}"))?;
s.write_all(&data)
.map_err(|e| anyhow::anyhow!("write_all {path}: {e}"))?;
}
out.flush().map_err(|e| anyhow::anyhow!("cfb flush: {e}"))?;
Ok(())
}
fn build_basic_file_info(spec: &FixtureSpec) -> Vec<u8> {
let mut s = String::new();
s.push_str(&format!("{} 20230101_0000(x64)Z ", spec.year));
s.push_str(&format!(
"C:\\synthetic\\rvt-rs\\fixtures\\{}.rfa ENU",
spec.name
));
let guid = synthetic_guid(&spec.name, spec.seed);
s.push(' ');
s.push_str(&guid);
utf16le(&s)
}
fn utf16le(s: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(s.len() * 2);
for unit in s.encode_utf16() {
out.extend_from_slice(&unit.to_le_bytes());
}
out
}
fn synthetic_guid(name: &str, seed: u64) -> String {
let mut h = seed;
for b in name.bytes() {
h = h.wrapping_mul(1_000_003).wrapping_add(b as u64);
}
let a = (h & 0xffff_ffff) as u32;
let b = ((h >> 16) & 0xffff) as u16;
let c = ((h >> 8) & 0xffff) as u16;
let d = (h & 0xffff) as u16;
let e = h.wrapping_mul(0x9e37_79b9) & 0xffff_ffff_ffff;
format!("{a:08x}-{b:04x}-{c:04x}-{d:04x}-{e:012x}")
}
fn build_formats_latest(spec: &FixtureSpec) -> anyhow::Result<Vec<u8>> {
let mut body: Vec<u8> = Vec::new();
body.extend_from_slice(&[0u8; 8]);
for (idx, class_name) in spec.classes.iter().enumerate() {
let tag = 0x0100u16 + (idx as u16);
emit_class_record(
&mut body,
class_name,
tag,
"Element",
&synthesize_fields(class_name),
)?;
}
body.resize(body.len() + 64, 0u8);
compression::truncated_gzip_encode(&body).map_err(|e| anyhow::anyhow!("truncated_gzip: {e}"))
}
fn emit_class_record(
body: &mut Vec<u8>,
class_name: &str,
tag: u16,
parent_name: &str,
fields: &[SynthField],
) -> anyhow::Result<()> {
let name_bytes = class_name.as_bytes();
if name_bytes.len() > u16::MAX as usize {
anyhow::bail!("class name too long");
}
body.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
body.extend_from_slice(name_bytes);
body.extend_from_slice(&(tag | 0x8000).to_le_bytes());
body.extend_from_slice(&[0u8; 2]);
let parent_bytes = parent_name.as_bytes();
body.extend_from_slice(&(parent_bytes.len() as u16).to_le_bytes());
body.extend_from_slice(parent_bytes);
body.extend_from_slice(&[0u8; 2]);
let fc = fields.len() as u32;
body.extend_from_slice(&fc.to_le_bytes());
body.extend_from_slice(&fc.to_le_bytes());
for f in fields {
let fname = f.name.as_bytes();
body.extend_from_slice(&(fname.len() as u32).to_le_bytes());
body.extend_from_slice(fname);
body.extend_from_slice(&f.encoding);
}
Ok(())
}
struct SynthField {
name: &'static str,
encoding: [u8; 4],
value_bytes: Vec<u8>,
}
fn synthesize_fields(class_name: &str) -> Vec<SynthField> {
let mut out: Vec<SynthField> = vec![
SynthField {
name: "m_flag",
encoding: [0x01, 0x00, 0x00, 0x00],
value_bytes: vec![0x01],
},
SynthField {
name: "m_id",
encoding: [0x05, 0x00, 0x00, 0x00],
value_bytes: 42u32.to_le_bytes().to_vec(),
},
SynthField {
name: "m_guid",
encoding: [0x09, 0x00, 0x00, 0x00],
value_bytes: (0u8..16u8).collect(),
},
];
if matches!(class_name, "Wall" | "Level" | "Column" | "Beam" | "Slab") {
out.push(SynthField {
name: "m_height",
encoding: [0x07, 0x00, 0x00, 0x00],
value_bytes: 10.0_f64.to_le_bytes().to_vec(),
});
}
if class_name == "Project" {
out.push(SynthField {
name: "m_versionStamp",
encoding: [0x0b, 0x00, 0x00, 0x00],
value_bytes: 2024_i64.to_le_bytes().to_vec(),
});
}
out
}
fn build_global_latest(spec: &FixtureSpec) -> anyhow::Result<Vec<u8>> {
let mut body: Vec<u8> = Vec::new();
body.extend_from_slice(&[0u8; 0x20]);
let mut rng = spec.seed.wrapping_mul(0x9e37_79b9).wrapping_add(1);
for idx in 0..spec.element_count {
let class_name = &spec.classes[idx % spec.classes.len()];
let fields = synthesize_fields(class_name);
let element_id = (idx as u32).wrapping_add(1);
body.extend_from_slice(&element_id.to_le_bytes());
body.extend_from_slice(&((idx % spec.classes.len()) as u32).to_le_bytes());
for f in &fields {
if f.name == "m_id" {
let fresh = rng.wrapping_mul(1_103_515_245).wrapping_add(12345);
rng = fresh;
let v = (fresh >> 16) as u32;
body.extend_from_slice(&v.to_le_bytes());
} else {
body.extend_from_slice(&f.value_bytes);
}
}
}
body.resize(body.len() + 64, 0u8);
compression::truncated_gzip_encode_with_prefix8(&body)
.map_err(|e| anyhow::anyhow!("truncated_gzip prefix8: {e}"))
}
fn build_revit_preview() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0x62, 0x19, 0x22, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
out.extend_from_slice(&MIN_PNG_1X1);
out
}
const MIN_PNG_1X1: [u8; 67] = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ];
fn build_part_atom() -> Vec<u8> {
let xml = r#"<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:a="urn:schemas-autodesk-com:partatom">
<title>synthetic fixture</title>
<id>urn:synthetic:rvt-rs:fixture</id>
<updated>2026-04-20T00:00:00Z</updated>
<category term="23.40.20.00" scheme="OmniClass"/>
</entry>
"#;
xml.as_bytes().to_vec()
}
fn is_valid_class_name(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
if !bytes[0].is_ascii_uppercase() {
return false;
}
bytes[1..]
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_')
}