1use std::io::{self, Read};
11use std::path::Path;
12
13use anyhow::{anyhow, bail, Result};
14
15use super::{container, tagged};
16use crate::nxasm::{Component, Kind};
17
18pub const COMPONENTS: &str = "NeuAsmSld";
20const MAX_FILE: u64 = 64 * 1024 * 1024;
22const MAX_COMPONENTS: usize = 100_000;
24const MAX_NAME: usize = 255;
26
27pub fn components(data: &[u8]) -> Result<Vec<Component>> {
30 if !container::is_creo(data) {
31 bail!("not a Creo file (the file does not start with '#UGC:')");
32 }
33 let toc = container::toc(data)?;
34 let section = container::section(data, &toc, COMPONENTS)?
35 .ok_or_else(|| anyhow!("The Creo file has no component list ({COMPONENTS})."))?;
36 let tree = tagged::parse(§ion)?;
37 let mm = super::unit_mm(data, &toc);
38 let root = tree.root();
39 let top = tree.child(root, "neut_assem").unwrap_or(root);
40 let Some(members) = tree.child(top, "members") else {
41 return Ok(Vec::new());
42 };
43 let mut out = Vec::new();
44 for &m in tree.list(members).iter().take(MAX_COMPONENTS) {
45 let Some(name) = tree.str_at(m, "name").filter(|n| is_model_name(n)) else {
46 continue;
47 };
48 let ext = match tree.int_at(m, "type") {
50 Some(2) => "prt",
51 Some(1) => "asm",
52 _ => continue,
53 };
54 let vector = |field: &str| -> Option<[f64; 3]> {
55 let v = tree.dbls(tree.child(m, field)?);
56 <[f64; 3]>::try_from(v.as_slice())
57 .ok()
58 .filter(|v| v.iter().all(|x| x.is_finite()))
59 };
60 let (Some(e1), Some(e2), Some(e3), Some(origin)) =
61 (vector("e1"), vector("e2"), vector("e3"), vector("origin"))
62 else {
63 continue;
64 };
65 out.push(Component {
66 name: format!("{name}.{ext}"),
67 rotation: [e1, e2, e3],
70 translation: origin.map(|x| x * mm),
71 layer: None,
72 occurrence: None,
73 document: None,
74 });
75 }
76 Ok(out)
77}
78
79fn is_model_name(name: &str) -> bool {
81 !name.is_empty()
82 && name.len() <= MAX_NAME
83 && !name
84 .chars()
85 .any(|c| c.is_control() || matches!(c, '/' | '\\' | ':'))
86}
87
88pub fn kind_of(path: &Path) -> io::Result<Kind> {
90 let mut data = Vec::new();
91 std::fs::File::open(path)?
92 .take(MAX_FILE)
93 .read_to_end(&mut data)?;
94 Ok(match components(&data) {
95 Ok(c) if !c.is_empty() => Kind::Assembly(c),
96 _ => Kind::Other,
97 })
98}
99
100pub type SampleMember<'a> = (&'a str, bool, [[f64; 3]; 3], [f64; 3]);
103
104#[doc(hidden)]
108pub fn sample(members: &[SampleMember], unit: &str) -> Vec<u8> {
109 let mut w = tagged::Writer::new("neut_assem");
110 w.begin("assem_top")
111 .int("model_class", 0)
112 .end()
113 .begin_array("members", members.len());
114 for (k, (name, assembly, axes, origin)) in members.iter().enumerate() {
115 let [e1, e2, e3] = axes;
116 w.element("members")
117 .int("id", 40 + k as i64)
118 .int("type", if *assembly { 1 } else { 2 })
119 .string("name", name)
120 .string("family_name", name)
121 .dbls("e1", e1)
122 .dbls("e2", e2)
123 .dbls("e3", e3)
124 .dbls("origin", origin)
125 .end();
126 }
127 if !members.is_empty() {
128 w.end();
129 }
130 let structure = w.finish();
131 let units = unit_section(unit);
132 sample_file(
133 "ASSEMBLY",
134 &[
135 (super::UNIT_SYSTEM, units.as_slice()),
136 (COMPONENTS, structure.as_slice()),
137 ],
138 )
139}
140
141#[doc(hidden)]
144pub fn unit_section(unit: &str) -> Vec<u8> {
145 let mut w = tagged::Writer::new("nd_unitsystemdef_l03");
146 w.int("format", 0).strings(
147 "string_xar",
148 &[
149 "nd_unitsystemdef_l03::UnitSystemDef_L03",
150 "Unit_Length",
151 "Length",
152 "Name",
153 "length",
154 "System",
155 "Scale",
156 "Offset",
157 unit,
158 "Expression",
159 ],
160 );
161 w.finish()
162}
163
164#[doc(hidden)]
167pub fn sample_file(kind: &str, sections: &[(&str, &[u8])]) -> Vec<u8> {
168 const ENTRY: usize = 81;
169 let header = format!("#UGC:2 {kind} 1966 1560 800 1 1 15 3800 2020281 0 \\\n");
170 let toc_head = format!("#UGC_TOC 1 {} {ENTRY}\n", sections.len());
171 let mut offset = header.len() + toc_head.len() + ENTRY * sections.len();
172 let mut table = Vec::new();
173 let mut bodies = Vec::new();
174 for (name, body) in sections {
175 let line = format!("#{name}\n");
176 let size = line.len() + body.len();
177 let mut rec = format!("{name} {offset:x} {size:x} {:x} 1966 9 -1", body.len());
178 rec.truncate(ENTRY - 1);
179 while rec.len() < ENTRY - 1 {
180 rec.push(' ');
181 }
182 rec.push('\n');
183 table.extend(rec.into_bytes());
184 bodies.extend(line.into_bytes());
185 bodies.extend(*body);
186 offset += size;
187 }
188 let mut out = header.into_bytes();
189 out.extend(toc_head.into_bytes());
190 out.extend(table);
191 out.extend(bodies);
192 out
193}
194
195#[cfg(test)]
196#[allow(clippy::indexing_slicing)]
197mod tests {
198 use super::*;
199
200 const IDENTITY: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
201 const QUARTER: [[f64; 3]; 3] = [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
203
204 #[test]
205 fn members_become_components_with_their_files_and_placements() {
206 let data = sample(
207 &[
208 ("MS_TIP", false, IDENTITY, [0.0; 3]),
209 ("MS_MID", false, QUARTER, [34.5, 0.0, -2.0]),
210 ("BODY_SUB", true, IDENTITY, [0.0, 10.0, 0.0]),
211 ],
212 "mm",
213 );
214 let c = components(&data).unwrap();
215 let names: Vec<_> = c.iter().map(|c| c.name.as_str()).collect();
216 assert_eq!(names, ["MS_TIP.prt", "MS_MID.prt", "BODY_SUB.asm"]);
217 assert_eq!(c[1].place([1.0, 0.0, 0.0]), [34.5, 1.0, -2.0]);
219 assert_eq!(c[1].place([0.0, 2.0, 0.0]), [32.5, 0.0, -2.0]);
220 assert_eq!(c[2].translation, [0.0, 10.0, 0.0]);
221 }
222
223 #[test]
224 fn placements_are_converted_to_millimetres() {
225 let data = sample(&[("BOLT", false, IDENTITY, [1.0, 2.0, 0.5])], "in");
226 let c = components(&data).unwrap();
227 assert_eq!(c[0].translation, [25.4, 50.8, 12.7]);
228 let toc = container::toc(&data).unwrap();
229 assert_eq!(super::super::unit_mm(&data, &toc), 25.4);
230 let bare = sample_file("ASSEMBLY", &[]);
232 assert_eq!(super::super::unit_mm(&bare, &[]), 1.0);
233 }
234
235 #[test]
236 fn unusable_members_are_skipped() {
237 let mut w = tagged::Writer::new("neut_assem");
238 w.begin_array("members", 4);
239 for (ty, name, axes) in [
241 (3, "ASM_DEF_CSYS", true),
242 (2, "C:\\x", true),
243 (2, "NO_AXES", false),
244 (2, "GOOD", true),
245 ] {
246 w.element("members").int("type", ty).string("name", name);
247 if axes {
248 w.dbls("e1", &[1.0, 0.0, 0.0])
249 .dbls("e2", &[0.0, 1.0, 0.0])
250 .dbls("e3", &[0.0, 0.0, 1.0]);
251 }
252 w.dbls("origin", &[0.0; 3]).end();
253 }
254 w.end();
255 let data = sample_file("ASSEMBLY", &[(COMPONENTS, w.finish().as_slice())]);
256 let c = components(&data).unwrap();
257 assert_eq!(c.len(), 1);
258 assert_eq!(c[0].name, "GOOD.prt");
259 }
260
261 #[test]
262 fn other_files_and_damage_are_errors_not_panics() {
263 assert!(components(b"solid stl").is_err());
264 assert!(components(&[]).is_err());
265 assert!(components(&sample_file("PART", &[])).is_err());
267 let data = sample(
268 &[
269 ("A", false, IDENTITY, [1.0; 3]),
270 ("B", true, QUARTER, [2.0; 3]),
271 ],
272 "in",
273 );
274 for cut in 0..data.len() {
275 let _ = components(&data[..cut]);
276 }
277 let mut damaged = data.clone();
278 for i in (0..damaged.len()).step_by(7) {
279 damaged[i] ^= 0x5a;
280 let _ = components(&damaged);
281 }
282 }
283
284 #[test]
285 fn kind_of_reads_the_file() {
286 let dir = std::env::temp_dir().join(format!("creo-asm-{}", std::process::id()));
287 std::fs::create_dir_all(&dir).unwrap();
288 let asm = dir.join("frame.asm");
289 std::fs::write(&asm, sample(&[("A", false, IDENTITY, [0.0; 3])], "mm")).unwrap();
290 assert!(matches!(kind_of(&asm).unwrap(), Kind::Assembly(c) if c.len() == 1));
291 let empty = dir.join("empty.asm");
292 std::fs::write(&empty, sample(&[], "mm")).unwrap();
293 assert!(matches!(kind_of(&empty).unwrap(), Kind::Other));
294 std::fs::remove_dir_all(&dir).unwrap();
295 }
296}