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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
use std::path::PathBuf;
// Core LibreDWG sources actually needed for DWG/DXF *reading*, mirroring the
// former WASM build's `emmake make -C src` scope (which itself excludes
// examples/programs/test/). JSON/GeoJSON in/out modules are excluded outright
// (not compiled-and-ifdef'd-out) since this project never enables them --
// matches the WASM build's `--disable-json` flag.
//
// encode.c/encode2.c/out_dxf.c/out_dxfb.c are *writer* sources, and nothing
// in this workspace writes DWG or DXF any more (the write API was removed --
// see CHANGELOG.md). They stay compiled because reading depends on them:
// dwg.c gates dxf_read_file() on USE_WRITE, in_dxf.c calls encode.c's
// in_postprocess_handles()/in_postprocess_SEQEND() (and dwg.c's own
// USE_WRITE-gated dwg_find_tablehandle_silent()), and out_dxf.c hosts
// dwg_convert_SAB_to_SAT1(), which the 3DSOLID wireframe extraction
// needs. Only the symbols in the bindgen allowlist below reach Rust, and no
// writer entry point is among them.
const LIBREDWG_SOURCES: &[&str] = &[
"bits.c",
"classes.c",
"codepages.c",
"common.c",
"decode.c",
"decode2.c",
"decode_r11.c",
"decode_r2007.c",
"dwg.c",
"dwg_api.c",
"dxfclasses.c",
"dynapi.c",
"encode.c",
"encode2.c",
"free.c",
"geom.c",
"hash.c",
"in_dxf.c",
"logging.c",
"objects.c",
"out_dxf.c",
"out_dxfb.c",
"print.c",
"reedsolomon.c",
];
fn main() {
// vendor-config/config.h hardcodes SIZEOF_SIZE_T to 8 with no platform
// branch (unlike its neighboring SIZEOF_WCHAR_T, which does branch on
// _WIN32) -- that value feeds LibreDWG's MAX_MEM_ALLOC allocation-size
// sanity gate (bits.h) and multiple word-aligned fast-path reads in
// bits.c. On a 32-bit target this crate has never been built or
// validated on, that mismatch would silently misalign those reads and
// make the allocation-size gate against attacker-controlled DWG size
// fields far too permissive, rather than failing to compile. Fail
// loudly here instead -- see docs/CAVEATS.md.
let pointer_width =
std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").expect("cargo sets this for build scripts");
assert_eq!(
pointer_width, "64",
"libredwg-sys only supports 64-bit targets: vendor-config/config.h hardcodes \
SIZEOF_SIZE_T=8 (and other 64-bit assumptions) with no 32-bit branch, unvalidated \
and unsafe to silently use on a {pointer_width}-bit target. See docs/CAVEATS.md."
);
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
// Build from the vendored copy inside the crate (vendor/libredwg), not
// the workspace's lib/libredwg git submodule -- a `cargo package`/publish
// tarball only contains files under the crate root, and a downstream
// consumer building from crates.io has no submodule (no .git at all) to
// fall back to. vendor/libredwg is a plain, git-tracked copy of exactly
// the submodule files this crate's build actually reaches (traced via
// the real #include graph, not just the top-level LIBREDWG_SOURCES
// list) -- see scripts/sync-libredwg-vendor.sh and docs/ARCHITECTURE.md's
// "Build" section for how to refresh it after a submodule update.
let libredwg_src = manifest_dir.join("vendor/libredwg/src");
let libredwg_include = manifest_dir.join("vendor/libredwg/include");
let vendor_config = manifest_dir.join("vendor-config");
let shim_dir = manifest_dir.join("shim");
// Drift detector: fail loudly if vendor/libredwg/src's .c file count
// doesn't match what this file expects to compile, rather than silently
// compiling a stale/incomplete vendored copy. See docs/ARCHITECTURE.md's
// "Build" section for the submodule-update / re-vendor procedure this is
// meant to catch early.
let actual_c_files: Vec<String> = std::fs::read_dir(&libredwg_src)
.expect("crates/libredwg-sys/vendor/libredwg/src should exist (checked into git -- see scripts/sync-libredwg-vendor.sh)")
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| n.ends_with(".c"))
.collect();
if actual_c_files.len() != LIBREDWG_SOURCES.len() {
panic!(
"vendor/libredwg/src/*.c file count changed ({} found, {} expected). The vendored \
copy was likely edited or re-synced with a different file set -- re-check the \
LIBREDWG_SOURCES list in build.rs against vendor/libredwg/src before proceeding \
(see docs/ARCHITECTURE.md).",
actual_c_files.len(),
LIBREDWG_SOURCES.len()
);
}
// Dev-only cross-check: when the lib/libredwg submodule is also checked
// out (local dev / CI clone with submodules, not a published-crate
// build), warn if its .c file set has drifted from the vendored copy --
// i.e. someone ran `git submodule update --remote` without re-running
// scripts/sync-libredwg-vendor.sh.
if let Some(repo_root) = manifest_dir.parent().and_then(|p| p.parent()) {
let submodule_src = repo_root.join("lib/libredwg/src");
if let Ok(entries) = std::fs::read_dir(&submodule_src) {
const EXCLUDED_JSON_SOURCES: &[&str] = &["in_json.c", "out_json.c", "out_geojson.c"];
let submodule_c_count = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| n.ends_with(".c"))
.count();
let expected_submodule_total = LIBREDWG_SOURCES.len() + EXCLUDED_JSON_SOURCES.len();
if submodule_c_count != expected_submodule_total {
println!(
"cargo:warning=lib/libredwg/src/*.c ({submodule_c_count} files) has drifted \
from vendor/libredwg/src ({expected_submodule_total} expected: {} compiled + \
{} excluded JSON). Upstream LibreDWG was likely updated -- re-run \
scripts/sync-libredwg-vendor.sh and update LIBREDWG_SOURCES in build.rs.",
LIBREDWG_SOURCES.len(),
EXCLUDED_JSON_SOURCES.len()
);
}
}
}
let mut build = cc::Build::new();
build
.include(&vendor_config)
.include(&libredwg_src)
.include(&libredwg_include)
.warnings(false); // upstream C source, not ours to keep warning-clean
if build.get_compiler().is_like_msvc() {
// Vendored LibreDWG source has a UTF-8 comment (decode.c) that MSVC's
// default code-page-based source interpretation misreads badly enough
// to swallow the following source line into the comment. /utf-8 fixes
// it and matches how every other toolchain already reads the file.
build.flag("/utf-8").flag("/std:c11");
}
for src in LIBREDWG_SOURCES {
build.file(libredwg_src.join(src));
}
build.file(shim_dir.join("uncad_shim.c"));
build.compile("libredwg");
// --- bindgen -----------------------------------------------------------
// Explicit --target so libclang follows the same ABI cl.exe used to
// actually compile the library (matters for anyone building on a
// different host/target combination than this was validated on).
let target = std::env::var("TARGET").expect("cargo sets TARGET for build scripts");
let bindings = bindgen::Builder::default()
.header(shim_dir.join("wrapper.h").to_string_lossy())
.clang_arg(format!("--target={target}"))
.clang_arg("-xc")
.clang_arg("-std=c11")
.clang_arg(format!("-I{}", vendor_config.display()))
.clang_arg(format!("-I{}", libredwg_src.display()))
.clang_arg(format!("-I{}", libredwg_include.display()))
.clang_arg(format!("-I{}", shim_dir.display()))
.allowlist_function("dwg_read_file")
.allowlist_function("dxf_read_file")
// Deliberately NOT allowlisted: dwg_write_file. It exists in the
// compiled library (config.h defines USE_WRITE, which dxf_read_file
// also needs -- see the LIBREDWG_SOURCES comment), but this
// workspace has no write path; keeping the binding out makes that a
// property of the crate rather than of its callers.
.allowlist_function("dwg_get_num_objects")
.allowlist_function("dwg_get_object")
.allowlist_function("dwg_object_get_fixedtype")
.allowlist_function("dwg_object_get_dxfname")
.allowlist_function("dwg_object_get_handle")
.allowlist_function("get_first_owned_entity")
.allowlist_function("get_next_owned_entity")
.allowlist_function("get_first_owned_subentity")
.allowlist_function("get_next_owned_subentity")
.allowlist_function("dwg_object_polyline_2d_get_numpoints")
.allowlist_function("dwg_object_polyline_2d_get_points")
.allowlist_function("dwg_object_polyline_3d_get_numpoints")
.allowlist_function("dwg_object_polyline_3d_get_points")
// Named explicitly because src/lib.rs's hand-written
// Dwg_MLINE_vertex refers to it, not only because the polyline
// accessors above return it.
.allowlist_type("dwg_point_3d")
.allowlist_function("uncad_object_entity_ptr")
.allowlist_function("uncad_object_object_ptr")
.allowlist_function("uncad_multileader_get_lines")
.allowlist_function("uncad_multileader_free_lines")
.allowlist_type("uncad_multileader_line_t")
.allowlist_function("dwg_free")
.allowlist_function("uncad_3dsolid_sab_to_sat_text")
.allowlist_function("uncad_free_sat_text")
.allowlist_function("dwg_dynapi_.*")
.allowlist_type("Dwg_Data")
.allowlist_type("Dwg_Object")
.allowlist_type("Dwg_Object_Type")
// Dwg_Object's `tio` union alone pulls in ~90 Dwg_Entity_*/
// Dwg_Object_* struct types (one pointer variant per DWG entity/object
// type), one of which bindgen cannot lay out cleanly. Its failure mode
// is silent and self-contradictory: it emits a 1-byte placeholder
// struct body but keeps the correct clang-computed size in the
// layout_tests() assertion, so that assertion always fails even though
// clang itself parses everything fine (a standalone clang compile with
// identical flags reports the right sizeof() for every one of these
// types).
//
// No field-level Rust access into these types is needed: entity data
// always goes through dwg_dynapi_entity_value()/
// dwg_dynapi_entity_field(), and the Dwg_Object fields this workspace
// reads (fixedtype, handle.value) come from C accessor functions. So
// the root structs are opaqued outright -- bindgen then represents
// them as correctly-sized `[u8; N]` blobs, safe to zero, stack- or
// heap-allocate and pass by pointer, without ever laying out the
// problematic union.
.opaque_type("_dwg_struct")
.opaque_type("Dwg_Data")
.opaque_type("dwg_data") // dwg_api.h's own separate `typedef struct _dwg_struct dwg_data;`
.opaque_type("_dwg_object")
.opaque_type("Dwg_Object")
.opaque_type("dwg_object") // dwg_api.h's own separate `typedef struct _dwg_object dwg_object;`
// Same reasoning: these are parameter or return types of
// otherwise-allowlisted functions even with Dwg_Object itself opaque.
// Dwg_Entity__3DSOLID stays listed because bindgen still reaches it
// transitively and emits it as an opaque blob, even though nothing
// allowlisted names it directly any more (the SAB conversion goes
// through the uncad_3dsolid_sab_to_sat_text shim, which takes a
// void*).
.opaque_type("_dwg_object_entity")
.opaque_type("Dwg_Object_Entity")
.opaque_type("_dwg_object_object")
.opaque_type("Dwg_Object_Object")
.opaque_type("_dwg_entity_3DSOLID")
.opaque_type("Dwg_Entity__3DSOLID")
.allowlist_type("DWG_ERROR")
.allowlist_type("Dwg_DYNAPI_field")
// Referenced by src/lib.rs's hand-written Dwg_HATCH_Path.
.allowlist_type("Dwg_HATCH_PolylinePath")
// The HATCH and MLINE sub-structs below hit the same bindgen
// struct-codegen failure as Dwg_Object, but unlike Dwg_Object this
// crate needs real field access into them: HATCH boundary-path
// geometry is not reachable through dynapi at all, which only exposes
// top-level entity fields by name. Opaquing them removes field access
// entirely, and opaquing the types they reference does not help
// either. So they are blocklisted here and hand-written in src/lib.rs
// against dwg.h's field layout, each with a compile-time assertion on
// clang's own sizeof() (the value bindgen's own layout_tests()
// reported before blocklisting).
//
// Several of them cascade for one specific reason: a `parent` field
// typed `struct _dwg_entity_HATCH *` or `struct _dwg_object_MLINESTYLE
// *` forces bindgen to materialize that parent as a real, non-opaque
// type, and the parent is what fails. The hand-written versions leave
// `parent` as an untyped `*mut c_void` to break the chain.
.blocklist_type("_dwg_HATCH_Path")
.blocklist_type("Dwg_HATCH_Path")
.blocklist_type("_dwg_HATCH_PathSeg")
.blocklist_type("Dwg_HATCH_PathSeg")
.blocklist_type("_dwg_HATCH_ControlPoint")
.blocklist_type("Dwg_HATCH_ControlPoint")
.blocklist_type("_dwg_HATCH_DefLine")
.blocklist_type("Dwg_HATCH_DefLine")
.blocklist_type("_dwg_HATCH_Color")
.blocklist_type("Dwg_HATCH_Color")
.blocklist_type("_dwg_MLINE_vertex")
.blocklist_type("Dwg_MLINE_vertex")
// Dwg_MLINE_line is referenced by Dwg_MLINE_vertex.lines and by
// nothing this crate allowlists, yet a Linux/gcc build still
// generates it -- with a dangling reference to the blocklisted
// _dwg_MLINE_vertex, which does not compile. Windows/MSVC does not.
.blocklist_type("_dwg_MLINE_line")
.blocklist_type("Dwg_MLINE_line")
.blocklist_type("_dwg_MLINESTYLE_line")
.blocklist_type("Dwg_MLINESTYLE_line")
.allowlist_type("Dwg_Handle")
.allowlist_type("_dwg_handle")
.allowlist_type("Dwg_Object_Ref")
.allowlist_type("_dwg_object_ref")
.allowlist_type("Dwg_Color")
.allowlist_type("_dwg_color")
.allowlist_type("DWG_COLOR_METHOD")
.allowlist_type("Dwg_Color_Method")
.allowlist_var("DWG_ERR_.*")
.derive_default(true)
.generate_comments(true)
.layout_tests(true)
.generate()
.expect("bindgen should generate libredwg bindings");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("failed to write bindings.rs");
println!("cargo:rerun-if-changed={}", shim_dir.display());
println!("cargo:rerun-if-changed={}", vendor_config.display());
// The vendored C sources are the actual compile input, and neither cc
// (for .file() sources) nor bindgen (without CargoCallbacks) registers
// them with Cargo. Once any rerun-if-changed is printed Cargo watches
// *only* the named paths, so without this line a refreshed vendor/ (from
// scripts/sync-libredwg-vendor.sh) silently reused stale objects and a
// stale bindings.rs until `cargo clean -p libredwg-sys`. A directory
// path makes Cargo scan the whole tree (112 files -- negligible).
println!(
"cargo:rerun-if-changed={}",
libredwg_src
.parent()
.expect("vendor/libredwg/src always has a parent directory")
.display()
);
}