1#![deny(
42 clippy::unwrap_used,
43 clippy::expect_used,
44 clippy::indexing_slicing,
45 clippy::panic,
46 clippy::unreachable
47)]
48
49use serde::Serialize;
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "snake_case")]
54pub enum FormatKind {
55 Part,
57 Assembly,
59 Drawing,
61 Model,
63 Exchange,
65 Mesh,
67 Visualization,
69 Electronics,
71 Simulation,
73 Result,
75 Manufacturing,
77 Document,
79 Other,
81}
82
83impl FormatKind {
84 pub const ALL: [FormatKind; 13] = [
85 FormatKind::Part,
86 FormatKind::Assembly,
87 FormatKind::Drawing,
88 FormatKind::Model,
89 FormatKind::Exchange,
90 FormatKind::Mesh,
91 FormatKind::Visualization,
92 FormatKind::Electronics,
93 FormatKind::Simulation,
94 FormatKind::Result,
95 FormatKind::Manufacturing,
96 FormatKind::Document,
97 FormatKind::Other,
98 ];
99
100 pub fn as_str(self) -> &'static str {
101 match self {
102 FormatKind::Part => "part",
103 FormatKind::Assembly => "assembly",
104 FormatKind::Drawing => "drawing",
105 FormatKind::Model => "model",
106 FormatKind::Exchange => "exchange",
107 FormatKind::Mesh => "mesh",
108 FormatKind::Visualization => "visualization",
109 FormatKind::Electronics => "electronics",
110 FormatKind::Simulation => "simulation",
111 FormatKind::Result => "result",
112 FormatKind::Manufacturing => "manufacturing",
113 FormatKind::Document => "document",
114 FormatKind::Other => "other",
115 }
116 }
117
118 pub fn label(self) -> &'static str {
119 match self {
120 FormatKind::Part => "Part",
121 FormatKind::Assembly => "Assembly",
122 FormatKind::Drawing => "Drawing",
123 FormatKind::Model => "CAD model",
124 FormatKind::Exchange => "Exchange format",
125 FormatKind::Mesh => "Mesh",
126 FormatKind::Visualization => "Visualisation",
127 FormatKind::Electronics => "Electronics",
128 FormatKind::Simulation => "Simulation",
129 FormatKind::Result => "Simulation result",
130 FormatKind::Manufacturing => "Manufacturing",
131 FormatKind::Document => "Document",
132 FormatKind::Other => "Other",
133 }
134 }
135}
136
137#[derive(Clone, Copy, Debug, Serialize)]
139pub struct CadSystem {
140 pub id: &'static str,
141 pub name: &'static str,
142 pub short: &'static str,
144 pub vendor: &'static str,
145 #[serde(skip)]
147 pub aliases: &'static [&'static str],
148}
149
150#[derive(Clone, Copy, Debug, Serialize)]
152pub struct CadFormat {
153 pub id: &'static str,
155 pub label: &'static str,
156 pub system: &'static str,
158 pub kind: FormatKind,
159 pub extensions: &'static [&'static str],
161 #[serde(skip)]
164 pub mime: &'static str,
165}
166
167const OCTET: &str = "application/octet-stream";
168
169macro_rules! system {
170 ($id:literal, $name:literal, $short:literal, $vendor:literal $(, $alias:literal)*) => {
171 CadSystem { id: $id, name: $name, short: $short, vendor: $vendor, aliases: &[$($alias),*] }
172 };
173}
174
175macro_rules! cad_format {
176 ($id:literal, $label:literal, $system:literal, $kind:ident, [$($ext:literal),+], $mime:expr) => {
177 CadFormat {
178 id: $id,
179 label: $label,
180 system: $system,
181 kind: FormatKind::$kind,
182 extensions: &[$($ext),+],
183 mime: $mime,
184 }
185 };
186}
187
188#[rustfmt::skip]
190pub const SYSTEMS: &[CadSystem] = &[
191 system!("solidworks", "SolidWorks", "SolidWorks", "Dassault Systèmes", "sw"),
192 system!("nx", "Siemens NX", "NX", "Siemens", "unigraphics", "ug", "nx cad"),
193 system!("teamcenter", "Siemens Teamcenter (JT, PLM XML)", "Teamcenter", "Siemens", "tc"),
194 system!("solid-edge", "Siemens Solid Edge", "Solid Edge", "Siemens", "solidedge"),
195 system!("inventor", "Autodesk Inventor", "Inventor", "Autodesk"),
196 system!("fusion", "Autodesk Fusion", "Fusion", "Autodesk", "fusion 360", "fusion360"),
197 system!("autocad", "AutoCAD (DWG, DXF)", "AutoCAD", "Autodesk"),
198 system!("creo", "PTC Creo (Pro/ENGINEER)", "Creo", "PTC", "pro/engineer", "proe", "pro/e", "creo parametric"),
199 system!("catia", "CATIA V5", "CATIA", "Dassault Systèmes", "catia v5", "3dexperience"),
200 system!("spaceclaim", "Ansys SpaceClaim / Discovery", "SpaceClaim", "Ansys", "discovery"),
201 system!("freecad", "FreeCAD", "FreeCAD", "open source"),
202 system!("tangency", "SolidCraft and FieldCraft", "SolidCraft", "Tangency Labs (open source)", "fieldcraft", "tangency"),
203 system!("rhino", "Rhino", "Rhino", "Robert McNeel & Associates", "rhinoceros"),
204 system!("alias", "Autodesk Alias", "Alias", "Autodesk", "alias autostudio"),
205 system!("openscad", "OpenSCAD", "OpenSCAD", "open source"),
206 system!("revit", "Autodesk Revit", "Revit", "Autodesk"),
207 system!("navisworks", "Autodesk Navisworks", "Navisworks", "Autodesk"),
208 system!("microstation", "Bentley MicroStation", "MicroStation", "Bentley Systems", "dgn"),
209 system!("archicad", "Graphisoft Archicad", "Archicad", "Graphisoft"),
210 system!("sketchup", "SketchUp", "SketchUp", "Trimble"),
211 system!("bim", "Open BIM (IFC)", "IFC", "buildingSMART", "open bim", "industry foundation classes"),
212 system!("blender", "Blender", "Blender", "Blender Foundation"),
213 system!("maya", "Autodesk Maya", "Maya", "Autodesk"),
214 system!("3dsmax", "Autodesk 3ds Max", "3ds Max", "Autodesk", "3dsmax", "3d studio max"),
215 system!("cinema4d", "Maxon Cinema 4D", "Cinema 4D", "Maxon", "c4d"),
216 system!("modo", "Foundry Modo", "Modo", "Foundry"),
217 system!("houdini", "SideFX Houdini", "Houdini", "SideFX"),
218 system!("exchange", "Neutral exchange formats", "Neutral", "ISO / vendor independent", "neutral"),
219 system!("mesh", "Mesh and 3D printing", "Mesh", "vendor independent", "3d printing"),
220 system!("visualization", "Visualisation formats", "Visualisation", "vendor independent", "visualization"),
221 system!("pointcloud", "Point clouds and scans", "Point cloud", "vendor independent", "scan", "lidar", "point clouds"),
222 system!("kicad", "KiCad", "KiCad", "open source"),
223 system!("altium", "Altium Designer", "Altium", "Altium"),
224 system!("eagle", "Eagle", "Eagle", "Autodesk"),
225 system!("pcb-fab", "PCB manufacturing (Gerber, drill)", "PCB fab", "vendor independent", "gerber"),
226 system!("rocketry", "Rocketry (OpenRocket, RockSim, RASAero)", "Rocketry", "various", "openrocket", "rocksim", "rasaero"),
227 system!("cae", "Simulation (ANSYS, Abaqus, COMSOL, Nastran, CalculiX, LS-DYNA)", "Simulation", "various", "ansys", "abaqus", "comsol", "nastran", "calculix", "ls-dyna", "lsdyna", "fea", "optistruct", "simcenter"),
228 system!("cfd", "CFD (OpenFOAM, Fluent, STAR-CCM+)", "CFD", "various", "openfoam", "fluent", "star-ccm+", "starccm"),
229 system!("cae-open", "Open simulation data (VTK, CGNS, Exodus II, Gmsh)", "VTK/CGNS", "vendor independent", "vtk", "paraview", "cgns", "exodus", "gmsh"),
230 system!("cam", "CNC and 3D printer programs (G-code)", "G-code", "vendor independent", "gcode", "cnc"),
231 system!("documents", "Documents and images", "Document", "various"),
232];
233
234#[rustfmt::skip]
237pub const FORMATS: &[CadFormat] = &[
238 cad_format!("solidworks-part", "SolidWorks part", "solidworks", Part, ["sldprt"], OCTET),
240 cad_format!("solidworks-assembly", "SolidWorks assembly", "solidworks", Assembly, ["sldasm"], OCTET),
241 cad_format!("solidworks-drawing", "SolidWorks drawing", "solidworks", Drawing, ["slddrw"], OCTET),
242 cad_format!("solidworks-template", "SolidWorks template / sheet format", "solidworks", Other, ["prtdot", "asmdot", "drwdot", "slddrt"], OCTET),
243 cad_format!("solidworks-library", "SolidWorks library feature / block", "solidworks", Other, ["sldlfp", "sldblk"], OCTET),
244 cad_format!("nx-part", "NX part", "nx", Model, ["prt"], OCTET),
246 cad_format!("nx-simulation", "NX simulation / FE model", "nx", Simulation, ["sim", "fem", "afm"], OCTET),
247 cad_format!("jt", "JT visualisation (ISO 14306)", "teamcenter", Visualization, ["jt"], "model/jt"),
249 cad_format!("plmxml", "Teamcenter PLM XML", "teamcenter", Exchange, ["plmxml"], "application/xml"),
250 cad_format!("solidedge-part", "Solid Edge part", "solid-edge", Part, ["par"], OCTET),
252 cad_format!("solidedge-sheetmetal", "Solid Edge sheet metal part", "solid-edge", Part, ["psm"], OCTET),
253 cad_format!("solidedge-assembly", "Solid Edge assembly", "solid-edge", Assembly, ["asm"], OCTET),
254 cad_format!("solidedge-weldment", "Solid Edge weldment", "solid-edge", Assembly, ["pwd"], OCTET),
255 cad_format!("solidedge-drawing", "Solid Edge drawing", "solid-edge", Drawing, ["dft"], OCTET),
256 cad_format!("inventor-part", "Inventor part", "inventor", Part, ["ipt"], OCTET),
258 cad_format!("inventor-assembly", "Inventor assembly", "inventor", Assembly, ["iam"], OCTET),
259 cad_format!("inventor-drawing", "Inventor drawing", "inventor", Drawing, ["idw"], OCTET),
260 cad_format!("inventor-presentation", "Inventor presentation", "inventor", Other, ["ipn"], OCTET),
261 cad_format!("inventor-project", "Inventor project file", "inventor", Other, ["ipj"], OCTET),
262 cad_format!("fusion-design", "Fusion design archive", "fusion", Model, ["f3d"], OCTET),
264 cad_format!("fusion-distributed", "Fusion distributed design (with references)", "fusion", Model, ["f3z"], OCTET),
265 cad_format!("fusion-drawing", "Fusion drawing archive", "fusion", Drawing, ["f2d"], OCTET),
266 cad_format!("dwg", "DWG drawing (AutoCAD, Inventor)", "autocad", Drawing, ["dwg"], "image/vnd.dwg"),
268 cad_format!("dxf", "DXF drawing exchange", "autocad", Drawing, ["dxf"], "image/vnd.dxf"),
269 cad_format!("autocad-template", "AutoCAD drawing template", "autocad", Other, ["dwt"], OCTET),
270 cad_format!("creo-part", "Creo part", "creo", Part, ["prt"], OCTET),
272 cad_format!("creo-assembly", "Creo assembly", "creo", Assembly, ["asm"], OCTET),
273 cad_format!("creo-drawing", "Creo drawing", "creo", Drawing, ["drw"], OCTET),
274 cad_format!("creo-format", "Creo drawing format", "creo", Other, ["frm"], OCTET),
275 cad_format!("creo-layout", "Creo layout", "creo", Other, ["lay"], OCTET),
276 cad_format!("creo-sketch", "Creo sketch", "creo", Other, ["sec"], OCTET),
277 cad_format!("creo-manufacturing", "Creo manufacturing model", "creo", Manufacturing, ["mfg"], OCTET),
278 cad_format!("creo-neutral", "Creo neutral file", "creo", Exchange, ["neu"], OCTET),
279 cad_format!("creo-view", "Creo View visualisation", "creo", Visualization, ["pvz", "pvs"], OCTET),
280 cad_format!("catia-part", "CATIA part", "catia", Part, ["catpart"], OCTET),
282 cad_format!("catia-product", "CATIA product (assembly)", "catia", Assembly, ["catproduct"], OCTET),
283 cad_format!("catia-drawing", "CATIA drawing", "catia", Drawing, ["catdrawing"], OCTET),
284 cad_format!("catia-cgr", "CATIA CGR visualisation", "catia", Visualization, ["cgr"], OCTET),
285 cad_format!("catia-analysis", "CATIA analysis", "catia", Simulation, ["catanalysis"], OCTET),
286 cad_format!("catia-process", "CATIA process", "catia", Manufacturing, ["catprocess"], OCTET),
287 cad_format!("catia-material", "CATIA material library", "catia", Other, ["catmaterial"], OCTET),
288 cad_format!("3dxml", "3D XML (3DEXPERIENCE)", "catia", Visualization, ["3dxml"], OCTET),
289 cad_format!("spaceclaim", "SpaceClaim document", "spaceclaim", Model, ["scdoc"], OCTET),
291 cad_format!("freecad", "FreeCAD document", "freecad", Model, ["fcstd"], OCTET),
293 cad_format!("solidcraft-design", "SolidCraft design", "tangency", Model, ["sc", "solidcraft"], OCTET),
294 cad_format!("fieldcraft-model", "FieldCraft analysis model", "tangency", Simulation, ["fieldcraft"], OCTET),
295 cad_format!("rhino", "Rhino 3D model", "rhino", Model, ["3dm"], OCTET),
296 cad_format!("alias-wire", "Alias wire file", "alias", Model, ["wire"], OCTET),
297 cad_format!("openscad", "OpenSCAD script", "openscad", Model, ["scad"], "text/plain"),
298 cad_format!("revit-project", "Revit project", "revit", Model, ["rvt"], OCTET),
300 cad_format!("revit-family", "Revit family (component)", "revit", Part, ["rfa"], OCTET),
301 cad_format!("revit-template", "Revit template", "revit", Other, ["rte", "rft"], OCTET),
302 cad_format!("navisworks-document", "Navisworks document", "navisworks", Visualization, ["nwd"], OCTET),
303 cad_format!("navisworks-cache", "Navisworks cache", "navisworks", Visualization, ["nwc"], OCTET),
304 cad_format!("navisworks-file-set", "Navisworks file set (with references)", "navisworks", Other, ["nwf"], OCTET),
305 cad_format!("dgn", "MicroStation design (DGN)", "microstation", Model, ["dgn"], OCTET),
306 cad_format!("archicad-project", "Archicad project", "archicad", Model, ["pln"], OCTET),
307 cad_format!("archicad-archive", "Archicad archive", "archicad", Model, ["pla"], OCTET),
308 cad_format!("sketchup", "SketchUp model", "sketchup", Model, ["skp"], OCTET),
309 cad_format!("ifc", "IFC building model (ISO 16739)", "bim", Exchange, ["ifc"], OCTET),
310 cad_format!("ifc-xml", "IFC building model (XML)", "bim", Exchange, ["ifcxml"], "application/xml"),
311 cad_format!("ifc-zip", "IFC building model (zipped)", "bim", Exchange, ["ifczip"], OCTET),
312 cad_format!("blender", "Blender scene", "blender", Model, ["blend"], OCTET),
314 cad_format!("maya-ascii", "Maya scene (ASCII)", "maya", Model, ["ma"], OCTET),
315 cad_format!("maya-binary", "Maya scene (binary)", "maya", Model, ["mb"], OCTET),
316 cad_format!("3dsmax-scene", "3ds Max scene", "3dsmax", Model, ["max"], OCTET),
317 cad_format!("3ds", "3DS mesh (legacy 3D Studio)", "3dsmax", Mesh, ["3ds"], "application/x-3ds"),
318 cad_format!("cinema4d", "Cinema 4D scene", "cinema4d", Model, ["c4d"], OCTET),
319 cad_format!("modo", "Modo scene", "modo", Model, ["lxo"], OCTET),
320 cad_format!("houdini", "Houdini scene", "houdini", Model, ["hip", "hipnc", "hiplc"], OCTET),
321 cad_format!("step", "STEP (ISO 10303)", "exchange", Exchange, ["step", "stp", "p21"], "model/step"),
323 cad_format!("iges", "IGES", "exchange", Exchange, ["iges", "igs"], "model/iges"),
324 cad_format!("parasolid-text", "Parasolid (text)", "exchange", Exchange, ["x_t", "xmt_txt"], "model/vnd.parasolid.transmit.text"),
325 cad_format!("parasolid-binary", "Parasolid (binary)", "exchange", Exchange, ["x_b", "xmt_bin"], "model/vnd.parasolid.transmit.binary"),
326 cad_format!("acis-text", "ACIS (text)", "exchange", Exchange, ["sat"], OCTET),
327 cad_format!("acis-binary", "ACIS (binary)", "exchange", Exchange, ["sab"], OCTET),
328 cad_format!("brep", "OpenCascade BREP", "exchange", Exchange, ["brep", "brp"], OCTET),
329 cad_format!("vda-fs", "VDA-FS", "exchange", Exchange, ["vda"], OCTET),
330 cad_format!("stl", "STL mesh", "mesh", Mesh, ["stl"], "model/stl"),
332 cad_format!("obj", "Wavefront OBJ mesh", "mesh", Mesh, ["obj"], "model/obj"),
333 cad_format!("3mf", "3MF (3D printing)", "mesh", Mesh, ["3mf"], "model/3mf"),
334 cad_format!("ply", "PLY mesh / point cloud", "mesh", Mesh, ["ply"], OCTET),
335 cad_format!("off", "OFF mesh", "mesh", Mesh, ["off"], OCTET),
336 cad_format!("amf", "AMF (3D printing)", "mesh", Mesh, ["amf"], OCTET),
337 cad_format!("gltf", "glTF", "visualization", Visualization, ["gltf"], "model/gltf+json"),
339 cad_format!("glb", "glTF binary", "visualization", Visualization, ["glb"], "model/gltf-binary"),
340 cad_format!("fbx", "FBX", "visualization", Visualization, ["fbx"], OCTET),
341 cad_format!("collada", "COLLADA", "visualization", Visualization, ["dae"], "model/vnd.collada+xml"),
342 cad_format!("usd", "USD scene (Universal Scene Description)", "visualization", Visualization, ["usd", "usda", "usdc"], OCTET),
343 cad_format!("usdz", "USDZ (AR)", "visualization", Visualization, ["usdz"], "model/vnd.usdz+zip"),
344 cad_format!("alembic", "Alembic cache", "visualization", Visualization, ["abc"], OCTET),
345 cad_format!("vrml", "VRML", "visualization", Visualization, ["wrl", "vrml"], "model/vrml"),
346 cad_format!("x3d", "X3D", "visualization", Visualization, ["x3d"], "model/x3d+xml"),
347 cad_format!("pdf-3d", "3D PDF (U3D / PRC)", "visualization", Visualization, ["pdf"], "application/pdf"),
348 cad_format!("e57", "E57 point cloud (ASTM E2807)", "pointcloud", Visualization, ["e57"], OCTET),
350 cad_format!("las", "LAS point cloud (LiDAR)", "pointcloud", Visualization, ["las"], OCTET),
351 cad_format!("laz", "LAZ point cloud (compressed LAS)", "pointcloud", Visualization, ["laz"], OCTET),
352 cad_format!("pts", "PTS point cloud", "pointcloud", Visualization, ["pts"], "text/plain"),
353 cad_format!("ptx", "PTX point cloud (scanner stations)", "pointcloud", Visualization, ["ptx"], "text/plain"),
354 cad_format!("xyz", "XYZ point cloud", "pointcloud", Visualization, ["xyz"], "text/plain"),
355 cad_format!("pcd", "PCD point cloud (PCL)", "pointcloud", Visualization, ["pcd"], OCTET),
356 cad_format!("kicad-pcb", "KiCad board", "kicad", Electronics, ["kicad_pcb"], OCTET),
358 cad_format!("kicad-schematic", "KiCad schematic", "kicad", Electronics, ["kicad_sch"], OCTET),
359 cad_format!("kicad-project", "KiCad project", "kicad", Electronics, ["kicad_pro"], OCTET),
360 cad_format!("kicad-library", "KiCad symbol / footprint library", "kicad", Electronics, ["kicad_sym", "kicad_mod"], OCTET),
361 cad_format!("altium-pcb", "Altium board", "altium", Electronics, ["pcbdoc"], OCTET),
362 cad_format!("altium-schematic", "Altium schematic", "altium", Electronics, ["schdoc"], OCTET),
363 cad_format!("altium-project", "Altium project", "altium", Electronics, ["prjpcb"], OCTET),
364 cad_format!("altium-library", "Altium library", "altium", Electronics, ["pcblib", "schlib", "intlib"], OCTET),
365 cad_format!("eagle-board", "Eagle board", "eagle", Electronics, ["brd"], OCTET),
366 cad_format!("eagle-schematic", "Eagle schematic", "eagle", Electronics, ["sch"], OCTET),
367 cad_format!("eagle-library", "Eagle library", "eagle", Electronics, ["lbr"], OCTET),
368 cad_format!("gerber", "Gerber (PCB manufacturing)", "pcb-fab", Electronics, ["gbr", "gerber", "ger"], OCTET),
369 cad_format!("excellon", "Excellon drill file", "pcb-fab", Electronics, ["drl", "xln"], OCTET),
370 cad_format!("openrocket", "OpenRocket design", "rocketry", Simulation, ["ork"], OCTET),
372 cad_format!("rocksim", "RockSim design", "rocketry", Simulation, ["rkt"], OCTET),
373 cad_format!("rasaero", "RASAero II project", "rocketry", Simulation, ["cdx1"], OCTET),
374 cad_format!("motor-eng", "Motor thrust curve (RASP .eng)", "rocketry", Simulation, ["eng"], "text/plain"),
375 cad_format!("motor-rse", "Motor thrust curve (RockSim .rse)", "rocketry", Simulation, ["rse"], "application/xml"),
376 cad_format!("ansys-workbench", "ANSYS Workbench project", "cae", Simulation, ["wbpj"], OCTET),
378 cad_format!("abaqus-cae", "Abaqus model database", "cae", Simulation, ["cae"], OCTET),
379 cad_format!("abaqus-input", "Abaqus input file", "cae", Simulation, ["inp"], "text/plain"),
380 cad_format!("comsol", "COMSOL model", "cae", Simulation, ["mph"], OCTET),
381 cad_format!("abaqus-odb", "Abaqus output database (results)", "cae", Result, ["odb"], OCTET),
382 cad_format!("abaqus-status", "Abaqus status file", "cae", Result, ["sta"], "text/plain"),
383 cad_format!("abaqus-message", "Abaqus message file", "cae", Result, ["msg"], "text/plain"),
384 cad_format!("ansys-result", "ANSYS result file", "cae", Result, ["rst"], OCTET),
385 cad_format!("ansys-thermal-result", "ANSYS thermal result file", "cae", Result, ["rth"], OCTET),
386 cad_format!("ansys-database", "ANSYS Mechanical APDL database", "cae", Simulation, ["db"], OCTET),
387 cad_format!("nastran-bulk", "Nastran input deck (bulk data)", "cae", Simulation, ["bdf", "nas", "dat"], "text/plain"),
388 cad_format!("nastran-op2", "Nastran binary results (OP2)", "cae", Result, ["op2"], OCTET),
389 cad_format!("nastran-f06", "Nastran print output (F06)", "cae", Result, ["f06"], "text/plain"),
390 cad_format!("nastran-f04", "Nastran execution summary (F04)", "cae", Result, ["f04"], "text/plain"),
391 cad_format!("nastran-punch", "Nastran punch output (PCH)", "cae", Result, ["pch"], "text/plain"),
392 cad_format!("optistruct-deck", "OptiStruct / HyperMesh input deck", "cae", Simulation, ["fem"], "text/plain"),
393 cad_format!("calculix-frd", "CalculiX results (FRD)", "cae", Result, ["frd"], "text/plain"),
394 cad_format!("lsdyna-keyword", "LS-DYNA keyword input deck", "cae", Simulation, ["k"], "text/plain"),
395 cad_format!("lsdyna-d3plot", "LS-DYNA results (d3plot)", "cae", Result, ["d3plot"], OCTET),
396 cad_format!("openfoam-case", "OpenFOAM case (controlDict, .foam marker)", "cfd", Simulation, ["foam"], "text/plain"),
398 cad_format!("fluent-case", "Fluent case file", "cfd", Simulation, ["cas"], OCTET),
399 cad_format!("fluent-data", "Fluent data file (results)", "cfd", Result, ["dat"], OCTET),
400 cad_format!("fluent-case-h5", "Fluent case file (HDF5)", "cfd", Simulation, ["cas.h5"], OCTET),
401 cad_format!("fluent-data-h5", "Fluent data file (HDF5, results)", "cfd", Result, ["dat.h5"], OCTET),
402 cad_format!("fluent-mesh", "Fluent mesh file", "cfd", Mesh, ["msh"], OCTET),
403 cad_format!("fluent-mesh-h5", "Fluent mesh file (HDF5)", "cfd", Mesh, ["msh.h5"], OCTET),
404 cad_format!("fluent-case-gz", "Fluent case file (gzip)", "cfd", Simulation, ["cas.gz"], "application/gzip"),
405 cad_format!("fluent-data-gz", "Fluent data file (gzip, results)", "cfd", Result, ["dat.gz"], "application/gzip"),
406 cad_format!("fluent-mesh-gz", "Fluent mesh file (gzip)", "cfd", Mesh, ["msh.gz"], "application/gzip"),
407 cad_format!("fluent-project", "Fluent project", "cfd", Simulation, ["flprj"], OCTET),
408 cad_format!("fluent-cdat", "Fluent CFD-Post data (results)", "cfd", Result, ["cdat"], OCTET),
409 cad_format!("fluent-journal", "Fluent journal (run script)", "cfd", Simulation, ["jou"], "text/plain"),
410 cad_format!("fluent-transcript", "Fluent transcript (solver log)", "cfd", Result, ["trn"], "text/plain"),
411 cad_format!("fluent-report", "Fluent report file (monitor values)", "cfd", Result, ["out"], "text/plain"),
412 cad_format!("starccm-sim", "STAR-CCM+ simulation", "cfd", Simulation, ["sim"], OCTET),
413 cad_format!("starccm-history", "STAR-CCM+ solution history (results)", "cfd", Result, ["simh"], OCTET),
414 cad_format!("starccm-macro", "STAR-CCM+ macro (Java)", "cfd", Simulation, ["java"], "text/plain"),
415 cad_format!("vtk-legacy", "VTK legacy data", "cae-open", Result, ["vtk"], OCTET),
417 cad_format!("vtk-unstructured", "VTK unstructured grid (XML)", "cae-open", Result, ["vtu"], "application/xml"),
418 cad_format!("vtk-polydata", "VTK polygonal data (XML)", "cae-open", Result, ["vtp"], "application/xml"),
419 cad_format!("vtk-structured", "VTK structured grid (XML)", "cae-open", Result, ["vts"], "application/xml"),
420 cad_format!("vtk-rectilinear", "VTK rectilinear grid (XML)", "cae-open", Result, ["vtr"], "application/xml"),
421 cad_format!("vtk-collection", "ParaView data collection (time steps)", "cae-open", Result, ["pvd"], "application/xml"),
422 cad_format!("vtk-multiblock", "VTK multiblock data set (XML)", "cae-open", Result, ["vtm"], "application/xml"),
423 cad_format!("cgns", "CGNS (CFD General Notation System)", "cae-open", Result, ["cgns"], OCTET),
424 cad_format!("exodus", "Exodus II mesh and results", "cae-open", Result, ["exo", "e"], OCTET),
425 cad_format!("gmsh", "Gmsh mesh", "cae-open", Mesh, ["msh"], OCTET),
426 cad_format!("gcode", "G-code program", "cam", Manufacturing, ["gcode", "gco", "ngc", "nc", "bgcode"], "text/plain"),
428 cad_format!("pdf", "PDF document", "documents", Document, ["pdf"], "application/pdf"),
430 cad_format!("word", "Text document", "documents", Document, ["docx", "doc", "odt", "rtf"], ""),
431 cad_format!("spreadsheet", "Spreadsheet", "documents", Document, ["xlsx", "xls", "ods"], ""),
432 cad_format!("csv", "CSV table", "documents", Document, ["csv"], "text/csv"),
433 cad_format!("presentation", "Presentation", "documents", Document, ["pptx", "ppt", "odp"], ""),
434 cad_format!("text", "Text file", "documents", Document, ["txt", "md"], "text/plain"),
435 cad_format!("png", "PNG image", "documents", Document, ["png"], "image/png"),
436 cad_format!("jpeg", "JPEG image", "documents", Document, ["jpg", "jpeg"], "image/jpeg"),
437 cad_format!("zip", "ZIP archive", "documents", Other, ["zip"], "application/zip"),
438 cad_format!("7z", "7-Zip archive", "documents", Other, ["7z"], "application/x-7z-compressed"),
439];
440
441pub const AMBIGUOUS: [&str; 9] = [
444 "prt", "asm", "pdf", "sim", "fem", "dat", "msh", "java", "out",
445];
446
447const CREO_ITERATED: [&str; 7] = ["prt", "asm", "drw", "frm", "lay", "sec", "mfg"];
449
450pub const HEAD_LEN: usize = 1024;
452
453const OLE_MAGIC: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
454
455pub fn by_id(id: &str) -> Option<&'static CadFormat> {
456 FORMATS.iter().find(|f| f.id == id)
457}
458
459pub fn system(id: &str) -> Option<&'static CadSystem> {
460 SYSTEMS.iter().find(|s| s.id == id)
461}
462
463fn first_with_extension(ext: &str) -> Option<&'static CadFormat> {
464 FORMATS.iter().find(|f| f.extensions.contains(&ext))
465}
466
467pub fn creo_iteration(name: &str) -> Option<(&str, u32)> {
469 let (base, number) = name.rsplit_once('.')?;
470 if number.is_empty() || number.len() > 6 || !number.bytes().all(|b| b.is_ascii_digit()) {
471 return None;
472 }
473 let (stem, ext) = base.rsplit_once('.')?;
474 if stem.is_empty() || !CREO_ITERATED.contains(&ext.to_ascii_lowercase().as_str()) {
475 return None;
476 }
477 Some((base, number.parse().ok()?))
478}
479
480pub fn normalize_name(name: &str) -> &str {
482 creo_iteration(name).map_or(name, |(base, _)| base)
483}
484
485pub fn extension(name: &str) -> Option<String> {
488 let (stem, ext) = normalize_name(name).rsplit_once('.')?;
489 (!stem.is_empty() && !ext.is_empty()).then(|| ext.to_ascii_lowercase())
490}
491
492fn is_creo(head: &[u8]) -> bool {
493 head.starts_with(b"#UGC:")
494}
495
496fn is_ole(head: &[u8]) -> bool {
497 head.starts_with(&OLE_MAGIC)
498}
499
500fn looks_textual(head: &[u8]) -> bool {
502 !head.is_empty()
503 && !head.contains(&0)
504 && head
505 .iter()
506 .filter(|b| b.is_ascii_graphic() || b.is_ascii_whitespace() || **b >= 0x80)
507 .count()
508 * 100
509 >= head.len() * 95
510}
511
512pub fn detect(name: &str, head: &[u8]) -> Option<&'static CadFormat> {
515 if let Some(format) = detect_by_name(name) {
516 return Some(format);
517 }
518 let iterated = creo_iteration(name).is_some();
519 let known = !head.is_empty();
521 if let Some(compound) = compound_extension(name) {
522 return detect_compound(compound, head, known);
523 }
524 let ext = extension(name)?;
525 match ext.as_str() {
526 "sim" if is_nx_container(head) => by_id("nx-simulation"),
531 "sim" if contains_ascii_ci(head, b"star-ccm") || contains_ascii_ci(head, b"starccm") => {
532 by_id("starccm-sim")
533 }
534 "sim" => by_id("nx-simulation"),
535 "fem" if !known || is_nx_container(head) => by_id("nx-simulation"),
537 "fem" if looks_textual(head) => by_id("optistruct-deck"),
538 "fem" => None,
539 "afm" if known && !is_nx_container(head) => None,
541 "msg" | "sta" if known && !looks_textual(head) => None,
543 "f04" | "pch" | "jou" | "trn" if known && !looks_textual(head) => None,
545 "op2" if known && looks_textual(head) => None,
547 "db" if name.eq_ignore_ascii_case("thumbs.db")
550 || head.starts_with(b"SQLite format 3")
551 || is_ole(head)
552 || looks_textual(head) =>
553 {
554 None
555 }
556 "rst" if looks_textual(head) => None,
558 "bdf" if head.starts_with(b"STARTFONT") => None,
560 "odb" if head.starts_with(b"PK\x03\x04") => None,
562 "dat" if is_fluent_sections(head) => by_id("fluent-data"),
566 "dat" if looks_textual(head) && is_nastran_deck(head) => by_id("nastran-bulk"),
567 "dat" => None,
568 "msh" if is_fluent_mesh(head) => by_id("fluent-mesh"),
571 "msh" if trim_start(head).starts_with(b"$MeshFormat") => by_id("gmsh"),
572 "msh" => None,
573 "out" if is_fluent_report(head) => by_id("fluent-report"),
576 "out" if !known && is_report_file_name(name) => by_id("fluent-report"),
577 "out" => None,
578 "java" if is_starccm_macro(head) => by_id("starccm-macro"),
580 "java" => None,
581 "e" if known && !is_netcdf_or_hdf5(head) => None,
583 "k" if known && !looks_textual(head) => None,
585 "prt" if iterated || is_creo(head) => by_id("creo-part"),
588 "prt" => by_id("nx-part"),
589 "asm" if iterated || is_creo(head) => by_id("creo-assembly"),
592 "asm" if is_ole(head) => by_id("solidedge-assembly"),
593 "asm" if looks_textual(head) => None,
594 "sch" if head.starts_with(b"EESchema") => by_id("kicad-schematic"),
596 "obj" if !head.is_empty() && head.contains(&0) => None,
598 "pdf" => by_id("pdf"),
600 "sc" if head.is_empty() || head.starts_with(b"PK\x03\x04") || is_solidcraft_json(head) => {
603 by_id("solidcraft-design")
604 }
605 "sc" => None,
606 other => first_with_extension(other),
607 }
608}
609
610fn detect_by_name(name: &str) -> Option<&'static CadFormat> {
615 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
616 let lower = base.to_ascii_lowercase();
617 if base == "controlDict" {
618 return by_id("openfoam-case");
619 }
620 if let Some(rest) = lower.strip_prefix("d3plot") {
621 if rest.bytes().all(|b| b.is_ascii_digit()) {
622 return by_id("lsdyna-d3plot");
623 }
624 }
625 None
626}
627
628const COMPOUND: [&str; 6] = ["cas.h5", "dat.h5", "msh.h5", "cas.gz", "dat.gz", "msh.gz"];
631
632fn compound_extension(name: &str) -> Option<&'static str> {
635 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
636 let lower = base.to_ascii_lowercase();
637 COMPOUND.into_iter().find(|ext| {
638 lower.len() > ext.len() + 1
639 && lower.ends_with(ext)
640 && lower.as_bytes().get(lower.len() - ext.len() - 1) == Some(&b'.')
641 })
642}
643
644fn detect_compound(ext: &str, head: &[u8], known: bool) -> Option<&'static CadFormat> {
647 let id = match ext {
648 "cas.h5" => "fluent-case-h5",
649 "dat.h5" => "fluent-data-h5",
650 "msh.h5" => "fluent-mesh-h5",
651 "cas.gz" => "fluent-case-gz",
652 "dat.gz" => "fluent-data-gz",
653 "msh.gz" => "fluent-mesh-gz",
654 _ => return None,
655 };
656 let content_ok = if ext.ends_with(".h5") {
657 is_hdf5(head)
658 } else {
659 head.starts_with(&GZIP_MAGIC)
660 };
661 if known && !content_ok {
662 return None;
663 }
664 by_id(id)
665}
666
667const HDF5_SIGNATURE: &[u8; 8] = b"\x89HDF\r\n\x1a\n";
668const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
669
670fn is_hdf5(head: &[u8]) -> bool {
674 head.starts_with(HDF5_SIGNATURE) || head.get(512..520) == Some(HDF5_SIGNATURE.as_slice())
675}
676
677fn is_nx_container(head: &[u8]) -> bool {
680 head.starts_with(crate::nxpart::MAGIC)
681}
682
683fn trim_start(head: &[u8]) -> &[u8] {
685 let start = head
686 .iter()
687 .position(|b| !b.is_ascii_whitespace())
688 .unwrap_or(head.len());
689 head.get(start..).unwrap_or_default()
690}
691
692fn head_lines(head: &[u8]) -> impl Iterator<Item = &[u8]> {
694 head.split(|b| *b == b'\n')
695 .map(|l| l.strip_suffix(b"\r").unwrap_or(l))
696}
697
698fn is_nastran_deck(head: &[u8]) -> bool {
702 head_lines(head).any(|line| {
703 let line = trim_start(line);
704 let starts = |p: &[u8]| {
706 line.get(..p.len())
707 .is_some_and(|s| s.eq_ignore_ascii_case(p))
708 && (p.ends_with(b" ")
709 || !line
710 .get(p.len())
711 .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_'))
712 };
713 starts(b"SOL ")
714 || starts(b"CEND")
715 || starts(b"BEGIN BULK")
716 || starts(b"INIT MASTER")
717 || (line.starts_with(b"$")
718 && (contains_ascii_ci(line, b"nastran") || contains_ascii_ci(line, b"simcenter")))
719 })
720}
721
722fn is_fluent_mesh(head: &[u8]) -> bool {
725 let rest = trim_start(head);
726 is_fluent_sections(head) || rest.starts_with(b"(2 ") || rest.starts_with(b"(10 ")
727}
728
729fn quoted_tokens(line: &[u8]) -> Option<usize> {
733 let mut rest = trim_start(line);
734 if let Some(inner) = rest.strip_prefix(b"(") {
735 let inner = inner.trim_ascii_end();
736 rest = inner.strip_suffix(b")")?;
737 }
738 let mut count = 0;
739 loop {
740 rest = trim_start(rest);
741 if rest.is_empty() {
742 return (count > 0).then_some(count);
743 }
744 let after = rest.strip_prefix(b"\"")?;
745 let end = after.iter().position(|b| *b == b'"')?;
746 rest = after.get(end + 1..)?;
747 count += 1;
748 }
749}
750
751fn is_fluent_report(head: &[u8]) -> bool {
754 let mut lines = head_lines(head).filter(|l| !trim_start(l).is_empty());
755 matches!(lines.next().map(quoted_tokens), Some(Some(1)))
756 && matches!(lines.next().map(quoted_tokens), Some(Some(n)) if n >= 2)
757}
758
759fn is_report_file_name(name: &str) -> bool {
761 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
762 base.to_ascii_lowercase().starts_with("report-file")
763}
764
765fn is_starccm_macro(head: &[u8]) -> bool {
768 looks_textual(head)
769 && (contains_ascii_ci(head, b"star-ccm+") || contains_ascii_ci(head, b"import star.common"))
770}
771
772fn contains_ascii_ci(head: &[u8], needle: &[u8]) -> bool {
774 head.windows(needle.len())
775 .any(|w| w.eq_ignore_ascii_case(needle))
776}
777
778fn is_fluent_sections(head: &[u8]) -> bool {
781 let start = head
782 .iter()
783 .position(|b| !b.is_ascii_whitespace())
784 .unwrap_or(head.len());
785 let rest = head.get(start..).unwrap_or_default();
786 rest.starts_with(b"(0 ") || rest.starts_with(b"(1 ") || rest.starts_with(b"(4 ")
787}
788
789fn is_netcdf_or_hdf5(head: &[u8]) -> bool {
791 head.starts_with(b"CDF\x01")
792 || head.starts_with(b"CDF\x02")
793 || head.starts_with(b"CDF\x05")
794 || head.starts_with(b"\x89HDF\r\n\x1a\n")
795}
796
797fn is_solidcraft_json(head: &[u8]) -> bool {
799 let text = String::from_utf8_lossy(head);
800 text.trim_start().starts_with('{')
801 && (text.contains("\"solidcraft/") || text.contains("\"solvecraft/"))
802}
803
804async fn pdf_has_3d(path: &std::path::Path) -> bool {
807 pdf_has_3d_within(path, PDF_3D_SCAN_LIMIT).await
808}
809
810const PDF_3D_SCAN_LIMIT: u64 = 512 * 1024 * 1024;
813
814async fn pdf_has_3d_within(path: &std::path::Path, limit: u64) -> bool {
816 use tokio::io::AsyncReadExt;
817 let Ok(mut file) = tokio::fs::File::open(path).await else {
818 return false;
819 };
820 let needles: [&[u8]; 2] = [b"/U3D", b"/PRC"];
821 let mut buf = vec![0u8; 256 * 1024];
822 let mut carry: Vec<u8> = Vec::new();
823 let mut scanned = 0u64;
824 while scanned < limit {
825 let n = match file.read(&mut buf).await {
826 Ok(0) | Err(_) => return false,
827 Ok(n) => n,
828 };
829 scanned += n as u64;
830 carry.extend_from_slice(buf.get(..n).unwrap_or_default());
831 if needles
832 .iter()
833 .any(|needle| carry.windows(needle.len()).any(|w| w == *needle))
834 {
835 return true;
836 }
837 let keep = carry.len().saturating_sub(8);
838 carry.drain(..keep);
839 }
840 false
841}
842
843pub fn content_type(format: Option<&CadFormat>, client: &str) -> String {
846 match format {
847 Some(f) if !f.mime.is_empty() => f.mime.to_string(),
848 _ => client.to_string(),
849 }
850}
851
852pub async fn detect_stored(path: &std::path::Path, name: &str) -> Option<&'static CadFormat> {
855 use tokio::io::AsyncReadExt;
856 let mut head = Vec::with_capacity(HEAD_LEN);
857 if let Ok(file) = tokio::fs::File::open(path).await {
858 let _ = file.take(HEAD_LEN as u64).read_to_end(&mut head).await;
859 }
860 let format = detect(name, &head);
861 if format.is_some_and(|f| f.id == "pdf") && pdf_has_3d(path).await {
862 return by_id("pdf-3d");
863 }
864 format
865}
866
867#[derive(Clone, Copy, Debug, Serialize)]
869pub struct FormatInfo {
870 pub id: &'static str,
871 pub label: &'static str,
872 pub system: &'static str,
873 pub system_name: &'static str,
874 pub system_short: &'static str,
875 pub kind: FormatKind,
876 pub kind_label: &'static str,
877 pub preview: Option<&'static str>,
879 pub thumbnail: bool,
881 pub family: Option<&'static str>,
884}
885
886impl CadFormat {
887 pub fn info(&'static self) -> FormatInfo {
888 let system = system(self.system);
889 FormatInfo {
890 id: self.id,
891 label: self.label,
892 system: self.system,
893 system_name: system.map_or("", |s| s.name),
894 system_short: system.map_or("", |s| s.short),
895 kind: self.kind,
896 kind_label: self.kind.label(),
897 preview: crate::preview::PreviewFormat::of_format(self.id).map(|p| p.as_str()),
898 thumbnail: crate::thumbnail::supports(self.id),
899 family: family(self.id).map(|s| s.id),
900 }
901 }
902}
903
904pub fn info(id: &str) -> Option<FormatInfo> {
906 by_id(id).map(CadFormat::info)
907}
908
909pub fn ids_of_system(system_id: &str) -> Vec<String> {
911 FORMATS
912 .iter()
913 .filter(|f| f.system == system_id)
914 .map(|f| f.id.to_string())
915 .collect()
916}
917
918fn squash(s: &str) -> String {
920 s.chars()
921 .filter(|c| c.is_alphanumeric())
922 .flat_map(char::to_lowercase)
923 .collect()
924}
925
926fn label_core(label: &str) -> &str {
929 label.split(" (").next().unwrap_or(label)
930}
931
932pub fn ids_named_by(query: &str) -> Vec<String> {
936 let q = squash(query);
937 if q.len() < 2 {
938 return Vec::new();
939 }
940 let systems: Vec<&str> = SYSTEMS
941 .iter()
942 .filter(|s| {
943 squash(s.id) == q
944 || squash(s.name) == q
945 || squash(s.short) == q
946 || s.aliases.iter().any(|a| squash(a) == q)
947 })
948 .map(|s| s.id)
949 .collect();
950 FORMATS
951 .iter()
952 .filter(|f| {
953 systems.contains(&f.system)
954 || squash(f.id) == q
955 || squash(f.label) == q
956 || squash(label_core(f.label)) == q
957 })
958 .map(|f| f.id.to_string())
959 .collect()
960}
961
962const NEUTRAL_SYSTEMS: [&str; 13] = [
973 "teamcenter",
974 "exchange",
975 "mesh",
976 "visualization",
977 "pointcloud",
978 "bim",
979 "pcb-fab",
980 "rocketry",
981 "cae",
982 "cfd",
983 "cae-open",
984 "cam",
985 "documents",
986];
987
988const NEUTRAL_FORMATS: [&str; 2] = ["dxf", "3ds"];
991
992pub fn family(format_id: &str) -> Option<&'static CadSystem> {
998 let format = by_id(format_id)?;
999 if NEUTRAL_FORMATS.contains(&format.id) || NEUTRAL_SYSTEMS.contains(&format.system) {
1000 return None;
1001 }
1002 system(format.system)
1003}
1004
1005pub fn families() -> Vec<&'static CadSystem> {
1008 SYSTEMS
1009 .iter()
1010 .filter(|s| {
1011 FORMATS
1012 .iter()
1013 .any(|f| family(f.id).is_some_and(|x| x.id == s.id))
1014 })
1015 .collect()
1016}
1017
1018pub fn is_family(id: &str) -> bool {
1020 families().iter().any(|s| s.id == id)
1021}
1022
1023#[derive(Clone, Debug, Default, PartialEq, Eq)]
1025pub struct FamilyGrouping {
1026 pub collapsed: Vec<bool>,
1029 pub other_families: Vec<&'static str>,
1032}
1033
1034impl FamilyGrouping {
1035 pub fn collapsed_count(&self) -> usize {
1037 self.collapsed.iter().filter(|c| **c).count()
1038 }
1039}
1040
1041pub fn group_by_family(preferred: &str, formats: &[&str]) -> FamilyGrouping {
1048 let none = || FamilyGrouping {
1049 collapsed: vec![false; formats.len()],
1050 other_families: Vec::new(),
1051 };
1052 if preferred.is_empty() || !is_family(preferred) {
1053 return none();
1054 }
1055 let families: Vec<Option<&'static CadSystem>> = formats.iter().map(|f| family(f)).collect();
1056 if !families.iter().flatten().any(|s| s.id == preferred) {
1057 return none();
1058 }
1059 let mut grouping = FamilyGrouping::default();
1060 for fam in families {
1061 let other = fam.filter(|s| s.id != preferred);
1062 if let Some(s) = other {
1063 if !grouping.other_families.contains(&s.short) {
1064 grouping.other_families.push(s.short);
1065 }
1066 }
1067 grouping.collapsed.push(other.is_some());
1068 }
1069 grouping
1070}
1071
1072pub fn catalogue() -> Vec<serde_json::Value> {
1074 SYSTEMS
1075 .iter()
1076 .map(|s| {
1077 let formats: Vec<_> = FORMATS
1078 .iter()
1079 .filter(|f| f.system == s.id)
1080 .map(|f| {
1081 let info = f.info();
1082 serde_json::json!({
1083 "id": f.id,
1084 "label": f.label,
1085 "kind": f.kind,
1086 "kind_label": f.kind.label(),
1087 "extensions": f.extensions,
1088 "preview": info.preview,
1089 "searchable_text": f.extensions.first().is_some_and(|e| crate::textindex::indexes(&format!("x.{e}"))),
1090 "ambiguous": f.extensions.iter().any(|e| AMBIGUOUS.contains(e)),
1091 "creo_iterations": f.extensions.iter().any(|e| CREO_ITERATED.contains(e)) && f.system == "creo",
1092 })
1093 })
1094 .collect();
1095 serde_json::json!({
1096 "id": s.id,
1097 "name": s.name,
1098 "short": s.short,
1099 "vendor": s.vendor,
1100 "formats": formats,
1101 })
1102 })
1103 .collect()
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109 use std::collections::{HashMap, HashSet};
1110
1111 fn id(name: &str, head: &[u8]) -> Option<&'static str> {
1112 detect(name, head).map(|f| f.id)
1113 }
1114
1115 #[test]
1116 fn native_formats_of_the_common_cad_programs() {
1117 let cases = [
1118 ("Bracket.SLDPRT", "solidworks-part"),
1119 ("Rover.sldasm", "solidworks-assembly"),
1120 ("Rover.SLDDRW", "solidworks-drawing"),
1121 ("valve.ipt", "inventor-part"),
1122 ("valve.IAM", "inventor-assembly"),
1123 ("valve.idw", "inventor-drawing"),
1124 ("engine.f3d", "fusion-design"),
1125 ("engine.f3z", "fusion-distributed"),
1126 ("Nozzle.CATPart", "catia-part"),
1127 ("Stage.CATProduct", "catia-product"),
1128 ("Stage.CATDrawing", "catia-drawing"),
1129 ("plate.par", "solidedge-part"),
1130 ("plate.psm", "solidedge-sheetmetal"),
1131 ("plate.dft", "solidedge-drawing"),
1132 ("frame.drw", "creo-drawing"),
1133 ("layout.dwg", "dwg"),
1134 ("cut.DXF", "dxf"),
1135 ("model.jt", "jt"),
1136 ("structure.plmxml", "plmxml"),
1137 ("rover.FCStd", "freecad"),
1138 ("hull.scdoc", "spaceclaim"),
1139 ("tower.rvt", "revit-project"),
1140 ("door.rfa", "revit-family"),
1141 ("site.dgn", "dgn"),
1142 ("coordination.nwd", "navisworks-document"),
1143 ("coordination.nwc", "navisworks-cache"),
1144 ("coordination.nwf", "navisworks-file-set"),
1145 ("house.pln", "archicad-project"),
1146 ("house.pla", "archicad-archive"),
1147 ("house.skp", "sketchup"),
1148 ("body.3dm", "rhino"),
1149 ("body.wire", "alias-wire"),
1150 ("scene.blend", "blender"),
1151 ("scene.ma", "maya-ascii"),
1152 ("scene.mb", "maya-binary"),
1153 ("scene.max", "3dsmax-scene"),
1154 ("scene.c4d", "cinema4d"),
1155 ("scene.lxo", "modo"),
1156 ("scene.hipnc", "houdini"),
1157 ("bracket.fieldcraft", "fieldcraft-model"),
1158 ("bracket.solidcraft", "solidcraft-design"),
1159 ];
1160 for (name, expected) in cases {
1161 assert_eq!(id(name, &[]), Some(expected), "{name}");
1162 }
1163 }
1164
1165 #[test]
1166 fn neutral_formats() {
1167 let cases = [
1168 ("a.step", "step"),
1169 ("a.STP", "step"),
1170 ("a.p21", "step"),
1171 ("a.igs", "iges"),
1172 ("a.IGES", "iges"),
1173 ("a.x_t", "parasolid-text"),
1174 ("a.X_B", "parasolid-binary"),
1175 ("a.xmt_txt", "parasolid-text"),
1176 ("a.sat", "acis-text"),
1177 ("a.sab", "acis-binary"),
1178 ("a.stl", "stl"),
1179 ("a.3mf", "3mf"),
1180 ("a.brep", "brep"),
1181 ("board.kicad_pcb", "kicad-pcb"),
1182 ("flight.ork", "openrocket"),
1183 ("M2000.eng", "motor-eng"),
1184 ("building.ifc", "ifc"),
1185 ("building.IFCZIP", "ifc-zip"),
1186 ("mesh.off", "off"),
1187 ("legacy.3ds", "3ds"),
1188 ("scene.dae", "collada"),
1189 ("scene.usdz", "usdz"),
1190 ("scene.usda", "usd"),
1191 ("anim.abc", "alembic"),
1192 ("scan.e57", "e57"),
1193 ("scan.las", "las"),
1194 ("scan.laz", "laz"),
1195 ("scan.pts", "pts"),
1196 ("scan.xyz", "xyz"),
1197 ("report.pdf", "pdf"),
1198 ];
1199 for (name, expected) in cases {
1200 assert_eq!(id(name, &[]), Some(expected), "{name}");
1201 }
1202 assert_eq!(id("README", &[]), None);
1203 assert_eq!(id(".stl", &[]), None, "hidden file without a name");
1204 assert_eq!(id("archive.unknownext", &[]), None);
1205 }
1206
1207 #[test]
1208 fn ambiguous_extensions_are_resolved_by_content() {
1209 assert_eq!(id("housing.prt", b"\x00\x01binary"), Some("nx-part"));
1211 assert_eq!(id("housing.prt", &[]), Some("nx-part"));
1212 assert_eq!(
1213 id("housing.prt", b"#UGC:2 PART 1182 500 10 1 1 15 2400"),
1214 Some("creo-part")
1215 );
1216 assert_eq!(id("housing.prt.3", b"\x00binary"), Some("creo-part"));
1217 assert_eq!(id("rover.asm", b"#UGC:2 ASSEMBLY 1"), Some("creo-assembly"));
1219 assert_eq!(id("rover.asm.12", &[]), Some("creo-assembly"));
1220 let mut ole = OLE_MAGIC.to_vec();
1221 ole.extend_from_slice(&[0; 16]);
1222 assert_eq!(id("rover.asm", &ole), Some("solidedge-assembly"));
1223 let mut printable_ole = OLE_MAGIC.to_vec();
1226 printable_ole.extend_from_slice(&[b'a'; 60]);
1227 assert_eq!(id("rover.asm", &printable_ole), Some("solidedge-assembly"));
1228 assert_eq!(id("startup.asm", b" .section .text\n mov r0, #1\n"), None);
1229 assert!(id("rover.asm", b"\x00\x01\x02binary").is_some());
1231 assert_eq!(
1233 id("power.sch", b"EESchema Schematic File Version 4"),
1234 Some("kicad-schematic")
1235 );
1236 assert_eq!(
1237 id("power.sch", b"<?xml version=\"1.0\"?><!DOCTYPE eagle"),
1238 Some("eagle-schematic")
1239 );
1240 assert_eq!(id("hull.obj", b"v 0 0 0\nv 1 0 0\n"), Some("obj"));
1242 assert_eq!(id("main.obj", b"\x4c\x01\x00\x00"), None);
1243 assert_eq!(
1245 id("bracket.sc", b"PK\x03\x04rest"),
1246 Some("solidcraft-design")
1247 );
1248 assert_eq!(
1249 id("bracket.sc", b"{\n \"format\": \"solidcraft/24\",\n"),
1250 Some("solidcraft-design")
1251 );
1252 assert_eq!(
1253 id("synth.sc", b"SynthDef(\\sine, { Out.ar(0, SinOsc.ar) })"),
1254 None
1255 );
1256 assert_eq!(id("model.pdf", b"%PDF-1.7"), Some("pdf"));
1258 }
1259
1260 #[tokio::test]
1261 async fn three_d_pdfs_are_found_by_content() {
1262 let dir = std::env::temp_dir().join(format!("teamcraft-pdf-{}", uuid::Uuid::new_v4()));
1263 std::fs::create_dir_all(&dir).unwrap();
1264 let flat = dir.join("flat.pdf");
1265 std::fs::write(&flat, b"%PDF-1.7\n1 0 obj << /Type /Page >> endobj\n%%EOF").unwrap();
1266 let mut model = b"%PDF-1.7\n".to_vec();
1267 model.extend(std::iter::repeat_n(b'x', 300 * 1024)); model.extend_from_slice(b"\n7 0 obj << /Type /3D /Subtype /U3D /Length 9 >>\nstream");
1269 let three_d = dir.join("model.pdf");
1270 std::fs::write(&three_d, &model).unwrap();
1271 assert_eq!(
1272 detect_stored(&flat, "flat.pdf").await.map(|f| f.id),
1273 Some("pdf")
1274 );
1275 assert_eq!(
1276 detect_stored(&three_d, "model.pdf").await.map(|f| f.id),
1277 Some("pdf-3d")
1278 );
1279 std::fs::remove_dir_all(&dir).ok();
1280 }
1281
1282 #[tokio::test]
1285 async fn three_d_markers_are_searched_up_to_the_limit_only() {
1286 use std::io::{Seek, SeekFrom, Write};
1287 let dir = std::env::temp_dir().join(format!("teamcraft-pdf-{}", uuid::Uuid::new_v4()));
1288 std::fs::create_dir_all(&dir).unwrap();
1289 let at = |offset: u64, name: &str| {
1290 let path = dir.join(name);
1291 let mut f = std::fs::File::create(&path).unwrap();
1292 f.write_all(b"%PDF-1.7\n").unwrap();
1293 f.seek(SeekFrom::Start(offset)).unwrap();
1294 f.write_all(b"/Subtype /U3D").unwrap();
1295 path
1296 };
1297 const MIB: u64 = 1024 * 1024;
1298 assert_eq!(PDF_3D_SCAN_LIMIT, 512 * MIB);
1299 let inside = at(MIB - 64, "inside.pdf");
1300 let past = at(MIB, "past-limit.pdf");
1301 assert!(pdf_has_3d_within(&inside, MIB).await, "within the limit");
1302 assert!(
1303 !pdf_has_3d_within(&past, MIB).await,
1304 "right after the limit: treated as a 2D document"
1305 );
1306 assert!(
1307 pdf_has_3d_within(&past, 2 * MIB).await,
1308 "found with a larger limit"
1309 );
1310 assert!(pdf_has_3d(&at(2 * MIB, "two-mib.pdf")).await);
1312 std::fs::remove_dir_all(&dir).ok();
1313 }
1314
1315 #[test]
1316 fn creo_iterations() {
1317 assert_eq!(creo_iteration("bracket.prt.7"), Some(("bracket.prt", 7)));
1318 assert_eq!(creo_iteration("Frame.ASM.12"), Some(("Frame.ASM", 12)));
1319 assert_eq!(creo_iteration("sheet.drw.1"), Some(("sheet.drw", 1)));
1320 assert_eq!(creo_iteration("bracket.prt"), None);
1321 assert_eq!(creo_iteration("report.pdf.2"), None, "only Creo files");
1322 assert_eq!(creo_iteration("v1.2"), None);
1323 assert_eq!(creo_iteration(".prt.1"), None);
1324 assert_eq!(creo_iteration("bracket.prt.x"), None);
1325 assert_eq!(normalize_name("bracket.prt.7"), "bracket.prt");
1326 assert_eq!(normalize_name("bracket.stp"), "bracket.stp");
1327 assert_eq!(extension("Bracket.PRT.3").as_deref(), Some("prt"));
1328 }
1329
1330 #[test]
1331 fn simulation_models_and_results() {
1332 let by_name = [
1333 ("mesh.vtk", "vtk-legacy"),
1334 ("field.VTU", "vtk-unstructured"),
1335 ("surface.vtp", "vtk-polydata"),
1336 ("grid.vts", "vtk-structured"),
1337 ("grid.vtr", "vtk-rectilinear"),
1338 ("run.pvd", "vtk-collection"),
1339 ("blocks.vtm", "vtk-multiblock"),
1340 ("job.frd", "calculix-frd"),
1341 ("model.op2", "nastran-op2"),
1342 ("model.f06", "nastran-f06"),
1343 ("model.bdf", "nastran-bulk"),
1344 ("model.nas", "nastran-bulk"),
1345 ("file.rst", "ansys-result"),
1346 ("file.rth", "ansys-thermal-result"),
1347 ("file.db", "ansys-database"),
1348 ("Job-1.odb", "abaqus-odb"),
1349 ("Job-1.sta", "abaqus-status"),
1350 ("Job-1.msg", "abaqus-message"),
1351 ("flow.cgns", "cgns"),
1352 ("mesh.exo", "exodus"),
1353 ("mesh.e", "exodus"),
1354 ("pipe.cas", "fluent-case"),
1355 ("pipe.cas.h5", "fluent-case-h5"),
1356 ("pipe-1-00100.DAT.H5", "fluent-data-h5"),
1357 ("bracket.sim", "nx-simulation"),
1358 ("bracket_fem1.fem", "nx-simulation"),
1359 ("crash.k", "lsdyna-keyword"),
1360 ("crash.d3plot", "lsdyna-d3plot"),
1361 ("d3plot", "lsdyna-d3plot"),
1362 ("d3plot07", "lsdyna-d3plot"),
1363 ("controlDict", "openfoam-case"),
1364 ("system/controlDict", "openfoam-case"),
1365 ("pitzDaily.foam", "openfoam-case"),
1366 ];
1367 for (name, expected) in by_name {
1368 assert_eq!(id(name, &[]), Some(expected), "{name}");
1369 }
1370 for (expected, kind) in [
1371 ("vtk-unstructured", FormatKind::Result),
1372 ("calculix-frd", FormatKind::Result),
1373 ("nastran-bulk", FormatKind::Simulation),
1374 ("fluent-data", FormatKind::Result),
1375 ("openfoam-case", FormatKind::Simulation),
1376 ] {
1377 assert_eq!(by_id(expected).unwrap().kind, kind, "{expected}");
1378 }
1379 assert_eq!(
1381 id("pipe.sim", b"\x00\x01STAR-CCM+ 2402 simulation"),
1382 Some("starccm-sim")
1383 );
1384 assert_eq!(id("pipe.sim", b"\x00\x01\x02NX"), Some("nx-simulation"));
1385 assert_eq!(id("pipe.dat", &[]), None, ".dat alone is not enough");
1386 assert_eq!(
1387 id("pipe.dat", b"(0 \"fluent data\")\n"),
1388 Some("fluent-data")
1389 );
1390 assert_eq!(
1391 id("pipe.dat", b" (1 \"ANSYS Fluent 2024\")"),
1392 Some("fluent-data")
1393 );
1394 assert_eq!(id("values.dat", b"1 2 3\n4 5 6\n"), None);
1395 assert_eq!(id("mesh.e", b"CDF\x02\x00\x00\x00\x01"), Some("exodus"));
1396 assert_eq!(id("mesh.e", b"\x89HDF\r\n\x1a\n\x00"), Some("exodus"));
1397 assert_eq!(id("notes.e", b"plain text"), None);
1398 assert_eq!(id("app.db", b"SQLite format 3\x00"), None);
1399 assert_eq!(id("Thumbs.db", &[]), None);
1400 assert_eq!(id("cache.db", &OLE_MAGIC), None);
1401 assert_eq!(
1402 id("file.db", b"\x00\x00\x00\x64ANSYS"),
1403 Some("ansys-database")
1404 );
1405 assert_eq!(
1406 id("README.rst", b"Title\n=====\n\nText"),
1407 None,
1408 "reStructuredText"
1409 );
1410 assert_eq!(
1411 id("file.rst", b"\x00\x00\x00\x64\x00\x00"),
1412 Some("ansys-result")
1413 );
1414 assert_eq!(id("mail.msg", &OLE_MAGIC), None, "Outlook message");
1415 assert_eq!(
1416 id("Job-1.msg", b" Abaqus 2024\n STEP 1\n"),
1417 Some("abaqus-message")
1418 );
1419 assert_eq!(id("font.bdf", b"STARTFONT 2.1\n"), None);
1420 assert_eq!(id("model.bdf", b"SOL 101\nCEND\n"), Some("nastran-bulk"));
1421 assert_eq!(id("base.odb", b"PK\x03\x04"), None, "OpenDocument database");
1422 assert_eq!(id("crash.k", b"*KEYWORD\n*NODE\n"), Some("lsdyna-keyword"));
1423 assert_eq!(id("crash.k", b"\x00\x01\x02\x03"), None);
1424 assert_eq!(id("d3plotter.txt", &[]), Some("text"), "not d3plot");
1425 assert_eq!(id(".cas.h5", &[]), None, "no stem");
1426 assert!(!ids_named_by("simulation").is_empty());
1428 assert!(ids_named_by("OpenFOAM").contains(&"openfoam-case".to_string()));
1429 assert!(ids_named_by("vtk").contains(&"vtk-unstructured".to_string()));
1430 assert!(ids_named_by("Nastran").contains(&"nastran-op2".to_string()));
1431 }
1432
1433 #[test]
1434 fn registry_is_consistent() {
1435 let mut ids = HashSet::new();
1436 let mut owners: HashMap<&str, Vec<&str>> = HashMap::new();
1437 for f in FORMATS {
1438 assert!(ids.insert(f.id), "duplicate format id {}", f.id);
1439 assert!(system(f.system).is_some(), "{}: unknown system", f.id);
1440 assert!(!f.extensions.is_empty(), "{}", f.id);
1441 for ext in f.extensions {
1442 assert_eq!(*ext, ext.to_ascii_lowercase(), "{}: lower case", f.id);
1443 assert!(!ext.starts_with('.'), "{}: without dot", f.id);
1444 owners.entry(ext).or_default().push(f.id);
1445 }
1446 assert!(
1447 f.mime.is_empty() || (f.mime.contains('/') && !f.mime.contains(' ')),
1448 "{}: content type",
1449 f.id
1450 );
1451 assert!(by_id(f.id).is_some_and(|g| g.label == f.label));
1452 }
1453 for (ext, formats) in owners {
1454 if AMBIGUOUS.contains(&ext) {
1455 assert!((1..=2).contains(&formats.len()), "{ext}: {formats:?}");
1457 } else {
1458 assert_eq!(formats.len(), 1, "extension {ext} belongs to {formats:?}");
1459 }
1460 }
1461 for s in SYSTEMS {
1462 assert!(
1463 FORMATS.iter().any(|f| f.system == s.id),
1464 "system {} has no formats",
1465 s.id
1466 );
1467 }
1468 }
1469
1470 #[test]
1471 fn the_programs_the_users_asked_for_are_covered() {
1472 for system_id in [
1473 "solidworks",
1474 "nx",
1475 "teamcenter",
1476 "inventor",
1477 "fusion",
1478 "creo",
1479 "catia",
1480 "solid-edge",
1481 "autocad",
1482 "exchange",
1483 ] {
1484 assert!(system(system_id).is_some(), "{system_id}");
1485 }
1486 for ext in [
1487 "sldprt",
1488 "sldasm",
1489 "slddrw",
1490 "prt",
1491 "ipt",
1492 "iam",
1493 "idw",
1494 "dwg",
1495 "f3d",
1496 "f3z",
1497 "asm",
1498 "drw",
1499 "catpart",
1500 "catproduct",
1501 "catdrawing",
1502 "par",
1503 "dft",
1504 "dxf",
1505 "stp",
1506 "step",
1507 "igs",
1508 "iges",
1509 "x_t",
1510 "x_b",
1511 "jt",
1512 "sat",
1513 "sab",
1514 "stl",
1515 "psm",
1516 "scdoc",
1517 "rvt",
1518 "rfa",
1519 "skp",
1520 "dgn",
1521 "ifc",
1522 "nwd",
1523 "nwc",
1524 "nwf",
1525 "pln",
1526 "pla",
1527 "3dm",
1528 "wire",
1529 "blend",
1530 "ma",
1531 "mb",
1532 "max",
1533 "3ds",
1534 "c4d",
1535 "lxo",
1536 "hip",
1537 "hipnc",
1538 "pdf",
1539 "obj",
1540 "fbx",
1541 "gltf",
1542 "glb",
1543 "3mf",
1544 "ply",
1545 "dae",
1546 "usd",
1547 "usdz",
1548 "amf",
1549 "wrl",
1550 "x3d",
1551 "e57",
1552 "pts",
1553 "xyz",
1554 "las",
1555 "laz",
1556 "abc",
1557 ] {
1558 assert!(
1559 detect(&format!("file.{ext}"), &[]).is_some()
1560 || detect(&format!("file.{ext}"), b"#UGC:2").is_some(),
1561 ".{ext}"
1562 );
1563 }
1564 }
1565
1566 #[test]
1567 fn content_types() {
1568 assert_eq!(content_type(by_id("step"), OCTET), "model/step");
1569 assert_eq!(
1570 content_type(by_id("solidworks-part"), "application/x-whatever"),
1571 OCTET
1572 );
1573 assert_eq!(
1574 content_type(by_id("word"), "application/msword"),
1575 "application/msword",
1576 "documents keep the client's type"
1577 );
1578 assert_eq!(content_type(None, "text/plain"), "text/plain");
1579 }
1580
1581 #[test]
1582 fn search_names() {
1583 let sw = ids_named_by("SolidWorks");
1584 assert!(sw.contains(&"solidworks-part".to_string()) && sw.len() == 5);
1585 assert!(ids_named_by("nx").contains(&"nx-part".to_string()));
1586 assert!(!ids_named_by("nx").contains(&"creo-part".to_string()));
1587 assert!(ids_named_by("Pro/E").contains(&"creo-part".to_string()));
1588 assert!(ids_named_by("solid edge").contains(&"solidedge-drawing".to_string()));
1589 assert!(ids_named_by("Fusion 360").contains(&"fusion-design".to_string()));
1590 assert_eq!(ids_named_by("CATIA product"), vec!["catia-product"]);
1591 assert_eq!(ids_named_by("jt"), vec!["jt"]);
1592 assert!(ids_named_by("step").contains(&"step".to_string()));
1593 assert!(ids_named_by("bracket").is_empty());
1594 assert!(ids_named_by("x").is_empty());
1595 assert!(ids_named_by("Autodesk Inventor").contains(&"inventor-part".to_string()));
1598 assert_eq!(ids_of_system("inventor").len(), 5);
1599 }
1600
1601 #[test]
1602 fn extensions_are_case_insensitive() {
1603 for name in ["Bracket.SLDPRT", "bracket.sldprt", "BRACKET.SldPrt"] {
1604 assert_eq!(id(name, &[]), Some("solidworks-part"), "{name}");
1605 }
1606 assert_eq!(id("Top.CATProduct", &[]), Some("catia-product"));
1607 assert_eq!(id("TOP.CATPRODUCT", &[]), Some("catia-product"));
1608 assert_eq!(extension("A.STEP").as_deref(), Some("step"));
1609 assert_eq!(extension("noext"), None);
1610 assert_eq!(extension("trailing."), None);
1611 assert_eq!(extension(".hidden"), None);
1612 }
1613
1614 #[test]
1615 fn name_only_detection_trusts_the_extension() {
1616 assert_eq!(id("Job-1.msg", &[]), Some("abaqus-message"));
1619 assert_eq!(id("Job-1.sta", &[]), Some("abaqus-status"));
1620 assert_eq!(id("crash.k", &[]), Some("lsdyna-keyword"));
1621 assert_eq!(id("mesh.e", &[]), Some("exodus"));
1622 assert_eq!(id("hull.obj", &[]), Some("obj"));
1623 assert_eq!(id("design.sc", &[]), Some("solidcraft-design"));
1624 assert_eq!(id("pipe.dat", &[]), None);
1626 }
1627
1628 #[test]
1629 fn whole_file_names_and_paths() {
1630 assert_eq!(id("case\\system\\controlDict", &[]), Some("openfoam-case"));
1631 assert_eq!(
1632 id("controldict", &[]),
1633 None,
1634 "OpenFOAM spells it controlDict"
1635 );
1636 assert_eq!(id("run/D3PLOT12", &[]), Some("lsdyna-d3plot"));
1637 assert_eq!(id("d3plot_a", &[]), None);
1638 assert_eq!(id("d3plot01.bak", &[]), None);
1639 assert_eq!(id("x.sim", b"..starccm+.."), Some("starccm-sim"));
1641 assert_eq!(id("x.dat", b"\n\t(4 (\"rp\"))"), Some("fluent-data"));
1643 assert_eq!(id("x.dat", b"(2 3)"), None);
1644 assert_eq!(id("x.e", b"CDF\x03"), None, "unknown netCDF version");
1645 }
1646
1647 #[test]
1650 fn simcenter_3d_files() {
1651 let mut nx = crate::nxpart::MAGIC.to_vec();
1652 nx.extend_from_slice(&[0, 0, 0, 1, 0xFF, 0x00]);
1653 for name in ["bracket_sim1.sim", "bracket_fem1.fem", "frame.afm"] {
1655 assert_eq!(id(name, &nx), Some("nx-simulation"), "{name}");
1656 assert_eq!(id(name, &[]), Some("nx-simulation"), "{name} by name");
1657 }
1658 let mut nx_star = nx.clone();
1660 nx_star.extend_from_slice(b"STAR-CCM+ import");
1661 assert_eq!(id("a.sim", &nx_star), Some("nx-simulation"));
1662 assert_eq!(
1664 id("old.sim", b"\x00\x00\x00\x07binary"),
1665 Some("nx-simulation")
1666 );
1667 assert_eq!(
1669 id(
1670 "bracket.fem",
1671 b"$$ HyperMesh OptiStruct deck\nBEGIN BULK\nGRID,1,,0.,0.,0.\n"
1672 ),
1673 Some("optistruct-deck")
1674 );
1675 assert_eq!(id("bracket.fem", b"\x00\x01\x02\x03\x04"), None);
1676 assert_eq!(
1678 id(
1679 "Helvetica.afm",
1680 b"StartFontMetrics 4.1\nFontName Helvetica\n"
1681 ),
1682 None
1683 );
1684 for deck in [
1686 &b"$ Simcenter Nastran input deck\nSOL 101\nCEND\n"[..],
1687 b"SOL 101\nCEND\nBEGIN BULK\n",
1688 b" sol 103\n",
1689 b"INIT MASTER(S)\nID bracket\n",
1690 b"TITLE = x\r\nBEGIN BULK\r\nGRID,1\r\n",
1691 b"$ NASTRAN deck\nGRID,1,,0.,0.,0.\n",
1692 ] {
1693 assert_eq!(
1694 id("bracket_sim1-solution_1.dat", deck),
1695 Some("nastran-bulk")
1696 );
1697 }
1698 assert_eq!(id("values.dat", b"SOLAR 1 2 3\nCENDRA\n"), None);
1699 assert_eq!(id("values.dat", b"$ just a comment\n1 2 3\n"), None);
1700 assert_eq!(id("blob.dat", b"\x00SOL 101\nCEND\n"), None, "binary");
1701 assert_eq!(
1702 id("pipe.dat", b"(0 \"fluent\")\nSOL 101\n"),
1703 Some("fluent-data")
1704 );
1705 assert_eq!(
1707 id("run.f04", b" NASTRAN EXECUTIVE SUMMARY\n"),
1708 Some("nastran-f04")
1709 );
1710 assert_eq!(id("run.f04", &[]), Some("nastran-f04"));
1711 assert_eq!(id("run.f04", b"\x00\x01\x02\x03"), None);
1712 assert_eq!(id("run.pch", b"$TITLE = x\n"), Some("nastran-punch"));
1713 assert_eq!(id("run.pch", b"\x00\x01\x02\x03"), None);
1714 assert_eq!(
1715 id(
1716 "run.op2",
1717 b"\x00\x00\x00\x1cNASTRAN FORT TAPE ID CODE - \x00"
1718 ),
1719 Some("nastran-op2")
1720 );
1721 assert_eq!(id("run.op2", &[]), Some("nastran-op2"));
1722 assert_eq!(id("notes.op2", b"plain text notes\n"), None);
1723 }
1724
1725 #[test]
1726 fn fluent_files() {
1727 let mut hdf5 = HDF5_SIGNATURE.to_vec();
1728 hdf5.extend_from_slice(&[0; 16]);
1729 let mut user_block = vec![0u8; 512];
1730 user_block.extend_from_slice(HDF5_SIGNATURE);
1731 let gz = [0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0];
1732 for (name, expected) in [
1733 ("pipe.cas.h5", "fluent-case-h5"),
1734 ("pipe-1-00100.dat.h5", "fluent-data-h5"),
1735 ("pipe.MSH.H5", "fluent-mesh-h5"),
1736 ] {
1737 assert_eq!(id(name, &hdf5), Some(expected), "{name}");
1738 assert_eq!(id(name, &user_block), Some(expected), "{name}, user block");
1739 assert_eq!(id(name, &[]), Some(expected), "{name} by name");
1740 assert_eq!(
1741 id(name, b"not hdf5 at all"),
1742 None,
1743 "{name} without signature"
1744 );
1745 assert_eq!(id(name, &gz), None, "{name} gzip");
1746 }
1747 for (name, expected) in [
1748 ("pipe.cas.gz", "fluent-case-gz"),
1749 ("pipe.dat.gz", "fluent-data-gz"),
1750 ("pipe.msh.gz", "fluent-mesh-gz"),
1751 ] {
1752 assert_eq!(id(name, &gz), Some(expected), "{name}");
1753 assert_eq!(id(name, &[]), Some(expected), "{name} by name");
1754 assert_eq!(id(name, &hdf5), None, "{name} without gzip magic");
1755 }
1756 assert_eq!(id(".msh.h5", &[]), None, "no stem");
1757 assert_eq!(id("pipe.xcas.h5", &[]), None, "not .cas.h5");
1758 assert_eq!(id("archive.gz", &gz), None);
1759 assert_eq!(
1761 id("pipe.msh", b"(0 \"Grid:\")\n(2 3)\n"),
1762 Some("fluent-mesh")
1763 );
1764 assert_eq!(
1765 id("pipe.msh", b"(2 3)\n(10 (0 1 2c8 0))\n"),
1766 Some("fluent-mesh")
1767 );
1768 assert_eq!(id("pipe.msh", b"\n(10 (0 1 2c8 0))\n"), Some("fluent-mesh"));
1769 assert_eq!(
1770 id("pipe.msh", b"$MeshFormat\n4.1 0 8\n$EndMeshFormat\n"),
1771 Some("gmsh")
1772 );
1773 assert_eq!(id("pipe.msh", b"random words\n"), None);
1774 assert_eq!(id("pipe.msh", &[]), None, "the name alone is ambiguous");
1775 assert_eq!(id("pipe.flprj", &[]), Some("fluent-project"));
1777 assert_eq!(id("pipe.cdat", &[]), Some("fluent-cdat"));
1778 assert_eq!(
1779 id(
1780 "run.jou",
1781 b"/file/read-case \"pipe.cas.h5\"\n/solve/iterate 500\n"
1782 ),
1783 Some("fluent-journal")
1784 );
1785 assert_eq!(id("run.jou", b"\x00\x01\x02\x03"), None);
1786 assert_eq!(
1787 id("run.trn", b"Welcome to ANSYS Fluent 2024 R2\n"),
1788 Some("fluent-transcript")
1789 );
1790 assert_eq!(id("run.trn", b"\x00\x01\x02\x03"), None);
1791 let report =
1793 b"\"report-def-0-rfile\"\n\"Iteration\" \"report-def-0\" \"flow-time\"\n1 0.5 0.001\n";
1794 assert_eq!(id("report-file-0.out", report), Some("fluent-report"));
1795 assert_eq!(
1796 id("monitor.out", report),
1797 Some("fluent-report"),
1798 "by content"
1799 );
1800 let repeated = b"\"title\"\r\n(\"Time Step\" \"drag\")\r\n1 2\r\n";
1801 assert_eq!(id("x.out", repeated), Some("fluent-report"));
1802 assert_eq!(id("report-file-0.out", &[]), Some("fluent-report"));
1803 assert_eq!(id("a.out", &[]), None);
1804 assert_eq!(id("a.out", b"\x7fELF\x02\x01\x01"), None, "an executable");
1805 assert_eq!(id("a.out", b"\"title\"\n1 2 3\n"), None, "no header");
1806 assert_eq!(id("a.out", b"\"a\" \"b\"\n\"c\" \"d\"\n"), None, "no title");
1807 assert_eq!(id("a.out", b"\"unclosed\n\"a\" \"b\n"), None);
1808 }
1809
1810 #[test]
1811 fn star_ccm_files() {
1812 assert_eq!(id("pipe.simh", &[]), Some("starccm-history"));
1813 let recorded = b"// Simcenter STAR-CCM+ macro: run.java\n// Written by Simcenter STAR-CCM+ 18.06.006\npackage macro;\n";
1814 assert_eq!(id("run.java", recorded), Some("starccm-macro"));
1815 assert_eq!(
1816 id("Export.java", b"package macro;\n\nimport star.common.*;\n"),
1817 Some("starccm-macro")
1818 );
1819 assert_eq!(
1820 id(
1821 "Main.java",
1822 b"package com.example;\n\npublic class Main {}\n"
1823 ),
1824 None,
1825 "ordinary Java"
1826 );
1827 assert_eq!(id("Main.java", &[]), None, "the name alone is not enough");
1828 assert_eq!(
1829 id("pipe.sim", b"\x00\x01Simcenter STAR-CCM+ 2402"),
1830 Some("starccm-sim")
1831 );
1832 }
1833
1834 #[test]
1835 fn solidcraft_json_needs_its_format_marker() {
1836 assert_eq!(
1837 id("a.sc", b" {\"format\": \"solvecraft/2\"}"),
1838 Some("solidcraft-design")
1839 );
1840 assert_eq!(id("a.sc", b"{\"format\": \"other/1\"}"), None);
1841 assert_eq!(id("a.sc", b"[\"solidcraft/1\"]"), None, "not an object");
1842 }
1843
1844 #[test]
1845 fn creo_iteration_limits() {
1846 assert_eq!(creo_iteration("a.prt.123456"), Some(("a.prt", 123_456)));
1847 assert_eq!(creo_iteration("a.prt.1234567"), None, "at most six digits");
1848 assert_eq!(creo_iteration("a.prt."), None);
1849 assert_eq!(creo_iteration("a.mfg.3"), Some(("a.mfg", 3)));
1850 assert_eq!(creo_iteration("a.MFG.3"), Some(("a.MFG", 3)));
1851 assert_eq!(id("plate.prt.12", b"\x00binary"), Some("creo-part"));
1853 assert_eq!(id("lay.lay.2", &[]), Some("creo-layout"));
1854 }
1855
1856 #[test]
1857 fn kinds_and_labels() {
1858 let names: HashSet<&str> = FormatKind::ALL.iter().map(|k| k.as_str()).collect();
1859 assert_eq!(names.len(), FormatKind::ALL.len(), "unique ids");
1860 let labels: HashSet<&str> = FormatKind::ALL.iter().map(|k| k.label()).collect();
1861 assert_eq!(labels.len(), FormatKind::ALL.len(), "unique labels");
1862 for f in FORMATS {
1863 assert!(FormatKind::ALL.contains(&f.kind));
1864 }
1865 for k in FormatKind::ALL {
1867 assert_eq!(
1868 serde_json::to_value(k).unwrap(),
1869 serde_json::Value::String(k.as_str().into())
1870 );
1871 }
1872 }
1873
1874 #[test]
1875 fn content_type_falls_back_to_the_client() {
1876 for id in ["word", "spreadsheet", "presentation"] {
1878 assert_eq!(content_type(by_id(id), "x/y"), "x/y", "{id}");
1879 }
1880 assert_eq!(content_type(by_id("nastran-bulk"), OCTET), "text/plain");
1881 }
1882
1883 #[test]
1884 fn search_ignores_case_spaces_and_punctuation() {
1885 assert_eq!(ids_named_by("SOLID WORKS"), ids_named_by("solidworks"));
1886 assert_eq!(ids_named_by("Solid-Edge"), ids_named_by("solid edge"));
1887 assert!(ids_named_by(" ").is_empty());
1888 let mut ids = ids_of_system("solidworks");
1889 ids.sort();
1890 let mut named = ids_named_by("SolidWorks");
1891 named.sort();
1892 assert_eq!(ids, named);
1893 assert!(ids_of_system("nope").is_empty());
1894 }
1895
1896 #[test]
1897 fn infos() {
1898 let product = info("catia-product").unwrap();
1899 assert_eq!(product.system_short, "CATIA");
1900 assert_eq!(product.kind, FormatKind::Assembly);
1901 assert_eq!(product.preview, Some("catia-product"));
1903 assert_eq!(info("catia-drawing").unwrap().preview, None);
1904 assert_eq!(ids_named_by("parasolid").len(), 2);
1905 assert_eq!(ids_named_by("NX part"), vec!["nx-part"]);
1906 assert_eq!(info("step").unwrap().preview, Some("step"));
1907 assert_eq!(info("3mf").unwrap().preview, Some("3mf"));
1908 assert_eq!(info("las").unwrap().preview, Some("las"));
1909 assert_eq!(
1910 info("brep").unwrap().preview,
1911 None,
1912 "no pure-Rust BREP reader"
1913 );
1914 assert_eq!(info("iges").unwrap().preview, None);
1915 assert_eq!(ids_named_by("revit").len(), 3);
1916 assert!(ids_named_by("point clouds").contains(&"e57".to_string()));
1917 assert_eq!(
1918 ids_named_by("FieldCraft"),
1919 vec!["solidcraft-design", "fieldcraft-model"]
1920 );
1921 assert!(info("").is_none() && info("nope").is_none());
1922 let catalogue = catalogue();
1923 assert_eq!(catalogue.len(), SYSTEMS.len());
1924 assert!(catalogue
1925 .iter()
1926 .all(|s| !s["formats"].as_array().unwrap().is_empty()));
1927 let listed: usize = catalogue
1929 .iter()
1930 .map(|s| s["formats"].as_array().unwrap().len())
1931 .sum();
1932 assert_eq!(listed, FORMATS.len(), "every format once");
1933 for s in &catalogue {
1934 for f in s["formats"].as_array().unwrap() {
1935 let id = f["id"].as_str().unwrap();
1936 assert_eq!(by_id(id).unwrap().system, s["id"], "{id}");
1937 }
1938 }
1939 }
1940
1941 #[test]
1943 fn quoted_tokens_counts_quoted_strings_only() {
1944 assert_eq!(quoted_tokens(br#""Iteration" "report-def-0""#), Some(2));
1945 assert_eq!(quoted_tokens(br#" ("Flow Time") "#), Some(1));
1946 assert_eq!(quoted_tokens(b"()"), None, "nothing inside the parentheses");
1947 assert_eq!(quoted_tokens(b" "), None);
1948 assert_eq!(quoted_tokens(br#""open"#), None, "unterminated");
1949 assert_eq!(quoted_tokens(br#""a" b"#), None, "unquoted token");
1950 assert_eq!(quoted_tokens(br#"("a""#), None, "unclosed parenthesis");
1951 }
1952
1953 #[test]
1955 fn text_needs_95_percent_printable_bytes_and_no_nul() {
1956 let with_controls = |n: usize| {
1957 let mut head = vec![b'a'; 20 - n];
1958 head.extend(std::iter::repeat_n(0x01, n));
1959 head
1960 };
1961 assert!(looks_textual(b"GRID 1 0.0 1.0\n"));
1962 assert!(
1963 looks_textual("Ümlaut-Kommentar\n".as_bytes()),
1964 "non-ASCII counts as text"
1965 );
1966 assert!(looks_textual(&with_controls(1)), "19 of 20 = 95 %");
1967 assert!(!looks_textual(&with_controls(2)), "18 of 20 = 90 %");
1968 assert!(!looks_textual(&[0x01; 20]));
1969 assert!(!looks_textual(b"text\0with a NUL"));
1970 assert!(!looks_textual(b""));
1971 }
1972
1973 #[rustfmt::skip]
1975 const EXPECTED_FAMILIES: &[(&str, &[&str])] = &[
1976 ("solidworks", &["solidworks-part", "solidworks-assembly", "solidworks-drawing", "solidworks-template", "solidworks-library"]),
1977 ("nx", &["nx-part", "nx-simulation"]),
1978 ("solid-edge", &["solidedge-part", "solidedge-sheetmetal", "solidedge-assembly", "solidedge-weldment", "solidedge-drawing"]),
1979 ("inventor", &["inventor-part", "inventor-assembly", "inventor-drawing", "inventor-presentation", "inventor-project"]),
1980 ("fusion", &["fusion-design", "fusion-distributed", "fusion-drawing"]),
1981 ("autocad", &["dwg", "autocad-template"]),
1982 ("creo", &["creo-part", "creo-assembly", "creo-drawing", "creo-format", "creo-layout", "creo-sketch", "creo-manufacturing", "creo-neutral", "creo-view"]),
1983 ("catia", &["catia-part", "catia-product", "catia-drawing", "catia-cgr", "catia-analysis", "catia-process", "catia-material", "3dxml"]),
1984 ("spaceclaim", &["spaceclaim"]),
1985 ("freecad", &["freecad"]),
1986 ("tangency", &["solidcraft-design", "fieldcraft-model"]),
1987 ("rhino", &["rhino"]),
1988 ("alias", &["alias-wire"]),
1989 ("openscad", &["openscad"]),
1990 ("revit", &["revit-project", "revit-family", "revit-template"]),
1991 ("navisworks", &["navisworks-document", "navisworks-cache", "navisworks-file-set"]),
1992 ("microstation", &["dgn"]),
1993 ("archicad", &["archicad-project", "archicad-archive"]),
1994 ("sketchup", &["sketchup"]),
1995 ("blender", &["blender"]),
1996 ("maya", &["maya-ascii", "maya-binary"]),
1997 ("3dsmax", &["3dsmax-scene"]),
1998 ("cinema4d", &["cinema4d"]),
1999 ("modo", &["modo"]),
2000 ("houdini", &["houdini"]),
2001 ("kicad", &["kicad-pcb", "kicad-schematic", "kicad-project", "kicad-library"]),
2002 ("altium", &["altium-pcb", "altium-schematic", "altium-project", "altium-library"]),
2003 ("eagle", &["eagle-board", "eagle-schematic", "eagle-library"]),
2004 ];
2005
2006 fn expected_family(format_id: &str) -> Option<&'static str> {
2007 EXPECTED_FAMILIES
2008 .iter()
2009 .find(|(_, ids)| ids.contains(&format_id))
2010 .map(|(family, _)| *family)
2011 }
2012
2013 #[test]
2014 fn every_registered_format_has_the_expected_family() {
2015 for f in FORMATS {
2016 assert_eq!(
2017 family(f.id).map(|s| s.id),
2018 expected_family(f.id),
2019 "family of {}",
2020 f.id
2021 );
2022 }
2023 let mut seen = HashSet::new();
2025 for (fam, ids) in EXPECTED_FAMILIES {
2026 assert!(system(fam).is_some(), "{fam} is a registered system");
2027 for id in *ids {
2028 assert!(by_id(id).is_some(), "{id} is registered");
2029 assert!(seen.insert(*id), "{id} listed twice");
2030 }
2031 }
2032 }
2033
2034 #[test]
2035 fn neutral_formats_belong_to_no_family() {
2036 for id in [
2037 "step",
2038 "iges",
2039 "parasolid-text",
2040 "jt",
2041 "plmxml",
2042 "stl",
2043 "3mf",
2044 "gltf",
2045 "pdf",
2046 "pdf-3d",
2047 "png",
2048 "jpeg",
2049 "text",
2050 "word",
2051 "zip",
2052 "dxf",
2053 "3ds",
2054 "ifc",
2055 "e57",
2056 "gerber",
2057 "abaqus-input",
2058 "fluent-case",
2059 "vtk-legacy",
2060 "gcode",
2061 "openrocket",
2062 ] {
2063 assert!(by_id(id).is_some(), "{id} is registered");
2064 assert_eq!(family(id).map(|s| s.id), None, "{id} is neutral");
2065 }
2066 assert!(family("").is_none(), "unknown format");
2067 assert!(family("no-such-format").is_none());
2068 }
2069
2070 #[test]
2071 fn families_are_the_programs_with_native_formats() {
2072 let ids: Vec<&str> = families().iter().map(|s| s.id).collect();
2073 let expected: Vec<&str> = SYSTEMS
2074 .iter()
2075 .map(|s| s.id)
2076 .filter(|id| EXPECTED_FAMILIES.iter().any(|(f, _)| f == id))
2077 .collect();
2078 assert_eq!(ids, expected, "in the order of SYSTEMS");
2079 assert!(is_family("nx") && is_family("solidworks") && is_family("kicad"));
2080 assert!(!is_family("exchange") && !is_family("teamcenter") && !is_family(""));
2081 assert_eq!(info("nx-part").and_then(|i| i.family), Some("nx"));
2082 assert_eq!(info("step").and_then(|i| i.family), None);
2083 }
2084
2085 #[test]
2086 fn grouping_collapses_other_families_when_the_preferred_one_is_present() {
2087 let files = [
2088 "nx-part",
2089 "solidworks-part",
2090 "step",
2091 "inventor-assembly",
2092 "nx-simulation",
2093 "solidworks-drawing",
2094 "pdf",
2095 "",
2096 ];
2097 let g = group_by_family("nx", &files);
2098 assert_eq!(
2099 g.collapsed,
2100 [false, true, false, true, false, true, false, false]
2101 );
2102 assert_eq!(g.other_families, ["SolidWorks", "Inventor"]);
2103 assert_eq!(g.collapsed_count(), 3);
2104 }
2105
2106 #[test]
2107 fn grouping_shows_everything_without_a_preferred_file() {
2108 let files = ["solidworks-part", "inventor-part", "step"];
2109 let g = group_by_family("nx", &files);
2110 assert_eq!(g.collapsed, [false, false, false]);
2111 assert!(g.other_families.is_empty());
2112 assert_eq!(g.collapsed_count(), 0);
2113 }
2114
2115 #[test]
2116 fn grouping_never_collapses_neutral_files() {
2117 let files = ["step", "stl", "pdf", "jt", "png"];
2118 for preferred in ["nx", "solidworks", ""] {
2119 let g = group_by_family(preferred, &files);
2120 assert_eq!(g.collapsed, [false; 5], "{preferred}");
2121 assert!(g.other_families.is_empty());
2122 }
2123 }
2124
2125 #[test]
2126 fn grouping_without_or_with_an_unknown_preference_shows_all() {
2127 let files = ["nx-part", "solidworks-part"];
2128 for preferred in ["", "exchange", "teamcenter", "nonsense"] {
2129 let g = group_by_family(preferred, &files);
2130 assert_eq!(g.collapsed, [false, false], "{preferred}");
2131 }
2132 assert_eq!(group_by_family("nx", &[]), FamilyGrouping::default());
2133 }
2134
2135 #[test]
2136 fn grouping_with_only_the_preferred_family_collapses_nothing() {
2137 let g = group_by_family(
2138 "solidworks",
2139 &["solidworks-part", "solidworks-drawing", "step"],
2140 );
2141 assert_eq!(g.collapsed, [false, false, false]);
2142 assert!(g.other_families.is_empty());
2143 }
2144}