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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
use std::path::PathBuf;
// Core LibreDWG sources actually needed for DWG/DXF read + DXF write, mirroring
// the existing 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.
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
// "빌드" 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
// "빌드" 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")
// Public, already-compiled (this crate's config.h defines USE_WRITE,
// required for dwg_write_file to even exist in dwg.c -- see
// docs/CAVEATS.md's DWG/DXF-writing section). Reliable for <= R_2004
// per LibreDWG's own README ("Write support only works for earlier
// versions until r2004"); despite the public `const Dwg_Data*`
// signature, dwg_write_file's own implementation casts away that
// const and mutates internally (`dwg_encode((Dwg_Data*)dwg, &dat)`,
// confirmed by reading dwg.c directly) -- CadDatabase::write_dwg
// takes `&mut self` to reflect that honestly rather than trust the
// C signature's claim.
.allowlist_function("dwg_write_file")
.allowlist_function("dwg_get_num_objects")
.allowlist_function("dwg_get_object")
.allowlist_function("dwg_object_get_fixedtype")
.allowlist_function("dwg_object_get_supertype")
.allowlist_function("dwg_object_get_dxfname")
.allowlist_function("dwg_object_get_name")
.allowlist_function("dwg_object_get_handle")
.allowlist_function("dwg_object_to_entity")
.allowlist_function("dwg_object_to_object")
.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")
.allowlist_type("dwg_point_2d")
.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("dwg_free_object")
.allowlist_function("dwg_abandon")
.allowlist_function("dwg_convert_SAB_to_SAT1")
.allowlist_function("dwg_dynapi_.*")
.allowlist_function("dwg_obj_.*")
.allowlist_function("dwg_ref_.*")
.allowlist_function("dwg_resolve_handleref")
.allowlist_function("uncad_write_dxf_file")
.allowlist_function("uncad_write_dxf")
.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 (root cause not isolated --
// candidates include the #pragma pack(1) bitfields + flexible-array-
// in-union EED record type reachable transitively). bindgen's failure
// mode for that 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 the
// assertion always fails even though clang itself parses everything
// fine (confirmed: a standalone clang.exe compile with identical
// flags reports the right sizeof() for every one of these types).
//
// This project's design never needed field-level Rust access into
// these types anyway -- entity data always goes through
// dwg_dynapi_entity_value()/dwg_dynapi_entity_field() (see the
// Dwg_Object.tio union-safety note in the plan), and Dwg_Object's own
// fields we need (fixedtype, handle.value) are read via the C
// accessor functions dwg_object_get_fixedtype()/dwg_obj_get_handle_value()
// rather than direct struct-field access, exactly like the existing
// JS binding already does. So: opaque the two root structs outright
// (bindgen then represents them as correctly-sized `[u8; N]` blobs,
// safe to stack-allocate/zero and pass by pointer to the C API,
// without ever trying to lay 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, needed because these are parameter/return types of
// otherwise-allowlisted functions (dwg_convert_SAB_to_SAT1,
// dwg_dynapi_*, dwg_obj_*) even with Dwg_Object itself opaque.
.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_Error")
.allowlist_type("Dwg_DYNAPI_field")
.allowlist_type("dwg_field_name_type_offset")
.allowlist_type("Dwg_HATCH_PolylinePath")
// Dwg_HATCH_Path/Dwg_HATCH_PathSeg hit the same bindgen struct-
// codegen limitation as Dwg_Object (see the big opaque_type block
// above) -- but unlike Dwg_Object, this project actually needs
// real field access into these two (HATCH boundary-path geometry
// isn't reachable through dynapi at all, since dynapi only exposes
// top-level entity fields by name, not nested struct internals).
// Opaquing them (tried first) doesn't help since that removes
// field access entirely, and neither does opaquing the types they
// reference (Dwg_HATCH_ControlPoint, Dwg_Entity_HATCH) -- the
// cascade's real trigger was never isolated. Blocklisted here and
// hand-defined instead, in src/lib.rs, matching dwg.h's field
// layout exactly (verified against clang's own authoritative
// sizeof(): 56 and 200 bytes respectively, both confirmed by the
// layout_tests() assertions bindgen itself generated before
// blocklisting -- i.e. clang parses these types fine, only
// bindgen's Rust codegen for them specifically fails).
.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")
// Dwg_HATCH_DefLine (the pattern-fill line-family definitions --
// angle/pt0/offset/dashes -- used to render actual hatch patterns
// instead of outline-only) is a top-level Dwg_Entity_HATCH field
// (num_deflines/deflines), reachable through dynapi unlike the
// Path/PathSeg/ControlPoint types above -- but allowlisting it
// directly was tried first and made things *worse*: its `parent`
// field is typed `struct _dwg_entity_HATCH *`, which forces bindgen
// to also generate a real (non-opaque) _dwg_entity_HATCH just to
// have a pointee type, and _dwg_entity_HATCH itself then hits the
// same struct-codegen failure (confirmed: allowlisting DefLine
// alone produced a `1usize - 200usize` layout_tests() assert on
// _dwg_entity_HATCH, which nothing else in this crate ever
// allowlists directly -- HATCH's own fields are all read through
// dynapi's untyped void*). Blocklisted and hand-defined instead,
// same as the three types above, with `parent` left as an untyped
// `*mut c_void` (matching Dwg_HATCH_ControlPoint/PathSeg/Path's own
// `parent` fields) specifically to avoid pulling _dwg_entity_HATCH
// in at all.
.blocklist_type("_dwg_HATCH_DefLine")
.blocklist_type("Dwg_HATCH_DefLine")
// Dwg_HATCH_Color (one gradient-fill color stop: shift_value +
// Dwg_Color) has the exact same `parent: struct _dwg_entity_HATCH *`
// cascade as Dwg_HATCH_DefLine right above -- allowlisting it forces
// bindgen to materialize a real _dwg_entity_HATCH, which fails the
// same way. Blocklisted and hand-defined instead, same recipe.
.blocklist_type("_dwg_HATCH_Color")
.blocklist_type("Dwg_HATCH_Color")
// Dwg_MLINE_vertex hits the exact same bindgen struct-codegen
// failure (confirmed: allowlisting it alone produces a
// layout_tests() assert computing `1usize - 96usize`, i.e. bindgen
// silently generated a 1-byte placeholder while clang's own
// sizeof() -- what the assert checks against -- is the correct 96).
// Blocklisted and hand-defined instead, same as the HATCH types
// above. Dwg_MLINE_line (referenced by Dwg_MLINE_vertex.lines, and
// by nothing this crate allowlists directly) also has to be
// blocklisted here -- confirmed via Docker Linux/gcc build that,
// unlike on the Windows/MSVC target, bindgen there still generates
// it (a dangling reference to the now-blocklisted
// _dwg_MLINE_vertex, a compile error) even though nothing
// allowlisted uses it; same class of platform-conditional bindgen
// divergence as docs/CAVEATS.md's other Windows/Linux notes.
.blocklist_type("_dwg_MLINE_vertex")
.blocklist_type("Dwg_MLINE_vertex")
.blocklist_type("_dwg_MLINE_line")
.blocklist_type("Dwg_MLINE_line")
// Dwg_MLINESTYLE_line (one parallel-line definition in an
// MLINESTYLE OBJECT -- offset/color/linetype -- read to give MLINE
// its real per-line offsets instead of centerline-only rendering)
// has the same `parent: struct _dwg_object_MLINESTYLE *` cascade as
// the HATCH sub-structs above (confirmed: allowlisting it alone
// produces a `1usize - 112usize` layout_tests() assert on
// _dwg_object_MLINESTYLE). Blocklisted and hand-defined instead,
// same recipe -- verified against clang's real sizeof() (80 bytes)
// the same way.
.blocklist_type("_dwg_MLINESTYLE_line")
.blocklist_type("Dwg_MLINESTYLE_line")
.allowlist_type("Dwg_Handle")
.allowlist_type("_dwg_handle")
.allowlist_type("Dwg_Object_Supertype")
.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_NOERR")
.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());
}