1 | /* |
2 | Open Asset Import Library (assimp) |
3 | ---------------------------------------------------------------------- |
4 | |
5 | Copyright (c) 2006-2017, assimp team |
6 | |
7 | All rights reserved. |
8 | |
9 | Redistribution and use of this software in source and binary forms, |
10 | with or without modification, are permitted provided that the |
11 | following conditions are met: |
12 | |
13 | * Redistributions of source code must retain the above |
14 | copyright notice, this list of conditions and the |
15 | following disclaimer. |
16 | |
17 | * Redistributions in binary form must reproduce the above |
18 | copyright notice, this list of conditions and the |
19 | following disclaimer in the documentation and/or other |
20 | materials provided with the distribution. |
21 | |
22 | * Neither the name of the assimp team, nor the names of its |
23 | contributors may be used to endorse or promote products |
24 | derived from this software without specific prior |
25 | written permission of the assimp team. |
26 | |
27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
28 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
29 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
30 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
31 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
32 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
33 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
34 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
35 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
36 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
37 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
38 | |
39 | ---------------------------------------------------------------------- |
40 | */ |
41 | |
42 | /** @file STEPFileReader.cpp |
43 | * @brief Implementation of the STEP file parser, which fills a |
44 | * STEP::DB with data read from a file. |
45 | */ |
46 | |
47 | #include "STEPFileReader.h" |
48 | #include "STEPFileEncoding.h" |
49 | #include "TinyFormatter.h" |
50 | #include "fast_atof.h" |
51 | #include <memory> |
52 | |
53 | |
54 | using namespace Assimp; |
55 | namespace EXPRESS = STEP::EXPRESS; |
56 | |
57 | #include <functional> |
58 | |
59 | // ------------------------------------------------------------------------------------------------ |
60 | std::string AddLineNumber(const std::string& s,uint64_t line /*= LINE_NOT_SPECIFIED*/, const std::string& prefix = "" ) |
61 | { |
62 | return line == STEP::SyntaxError::LINE_NOT_SPECIFIED ? prefix+s : static_cast<std::string>( (Formatter::format(),prefix,"(line " ,line,") " ,s) ); |
63 | } |
64 | |
65 | |
66 | // ------------------------------------------------------------------------------------------------ |
67 | std::string AddEntityID(const std::string& s,uint64_t entity /*= ENTITY_NOT_SPECIFIED*/, const std::string& prefix = "" ) |
68 | { |
69 | return entity == STEP::TypeError::ENTITY_NOT_SPECIFIED ? prefix+s : static_cast<std::string>( (Formatter::format(),prefix,"(entity #" ,entity,") " ,s)); |
70 | } |
71 | |
72 | |
73 | // ------------------------------------------------------------------------------------------------ |
74 | STEP::SyntaxError::SyntaxError (const std::string& s,uint64_t line /* = LINE_NOT_SPECIFIED */) |
75 | : DeadlyImportError(AddLineNumber(s,line)) |
76 | { |
77 | |
78 | } |
79 | |
80 | // ------------------------------------------------------------------------------------------------ |
81 | STEP::TypeError::TypeError (const std::string& s,uint64_t entity /* = ENTITY_NOT_SPECIFIED */,uint64_t line /*= LINE_NOT_SPECIFIED*/) |
82 | : DeadlyImportError(AddLineNumber(AddEntityID(s,entity),line)) |
83 | { |
84 | |
85 | } |
86 | |
87 | |
88 | // ------------------------------------------------------------------------------------------------ |
89 | STEP::DB* STEP::(std::shared_ptr<IOStream> stream) |
90 | { |
91 | std::shared_ptr<StreamReaderLE> reader = std::shared_ptr<StreamReaderLE>(new StreamReaderLE(stream)); |
92 | std::unique_ptr<STEP::DB> db = std::unique_ptr<STEP::DB>(new STEP::DB(reader)); |
93 | |
94 | LineSplitter& splitter = db->GetSplitter(); |
95 | if (!splitter || *splitter != "ISO-10303-21;" ) { |
96 | throw STEP::SyntaxError("expected magic token: ISO-10303-21" ,1); |
97 | } |
98 | |
99 | HeaderInfo& head = db->GetHeader(); |
100 | for(++splitter; splitter; ++splitter) { |
101 | const std::string& s = *splitter; |
102 | if (s == "DATA;" ) { |
103 | // here we go, header done, start of data section |
104 | ++splitter; |
105 | break; |
106 | } |
107 | |
108 | // want one-based line numbers for human readers, so +1 |
109 | const uint64_t line = splitter.get_index()+1; |
110 | |
111 | if (s.substr(0,11) == "FILE_SCHEMA" ) { |
112 | const char* sz = s.c_str()+11; |
113 | SkipSpaces(sz,&sz); |
114 | std::shared_ptr< const EXPRESS::DataType > schema = EXPRESS::DataType::Parse(sz); |
115 | |
116 | // the file schema should be a regular list entity, although it usually contains exactly one entry |
117 | // since the list itself is contained in a regular parameter list, we actually have |
118 | // two nested lists. |
119 | const EXPRESS::LIST* list = dynamic_cast<const EXPRESS::LIST*>(schema.get()); |
120 | if (list && list->GetSize()) { |
121 | list = dynamic_cast<const EXPRESS::LIST*>( (*list)[0].get() ); |
122 | if (!list) { |
123 | throw STEP::SyntaxError("expected FILE_SCHEMA to be a list" ,line); |
124 | } |
125 | |
126 | // XXX need support for multiple schemas? |
127 | if (list->GetSize() > 1) { |
128 | DefaultLogger::get()->warn(AddLineNumber("multiple schemas currently not supported" ,line)); |
129 | } |
130 | const EXPRESS::STRING* string( nullptr ); |
131 | if (!list->GetSize() || !(string=dynamic_cast<const EXPRESS::STRING*>( (*list)[0].get() ))) { |
132 | throw STEP::SyntaxError("expected FILE_SCHEMA to contain a single string literal" ,line); |
133 | } |
134 | head.fileSchema = *string; |
135 | } |
136 | } |
137 | |
138 | // XXX handle more header fields |
139 | } |
140 | |
141 | return db.release(); |
142 | } |
143 | |
144 | |
145 | namespace { |
146 | |
147 | // ------------------------------------------------------------------------------------------------ |
148 | // check whether the given line contains an entity definition (i.e. starts with "#<number>=") |
149 | bool IsEntityDef(const std::string& snext) |
150 | { |
151 | if (snext[0] == '#') { |
152 | // it is only a new entity if it has a '=' after the |
153 | // entity ID. |
154 | for(std::string::const_iterator it = snext.begin()+1; it != snext.end(); ++it) { |
155 | if (*it == '=') { |
156 | return true; |
157 | } |
158 | if ((*it < '0' || *it > '9') && *it != ' ') { |
159 | break; |
160 | } |
161 | } |
162 | } |
163 | return false; |
164 | } |
165 | |
166 | } |
167 | |
168 | |
169 | // ------------------------------------------------------------------------------------------------ |
170 | void STEP::ReadFile(DB& db,const EXPRESS::ConversionSchema& scheme, |
171 | const char* const* types_to_track, size_t len, |
172 | const char* const* inverse_indices_to_track, size_t len2) |
173 | { |
174 | db.SetSchema(scheme); |
175 | db.SetTypesToTrack(types_to_track,len); |
176 | db.SetInverseIndicesToTrack(inverse_indices_to_track,len2); |
177 | |
178 | const DB::ObjectMap& map = db.GetObjects(); |
179 | LineSplitter& splitter = db.GetSplitter(); |
180 | |
181 | while (splitter) { |
182 | bool has_next = false; |
183 | std::string s = *splitter; |
184 | if (s == "ENDSEC;" ) { |
185 | break; |
186 | } |
187 | s.erase(std::remove(s.begin(), s.end(), ' '), s.end()); |
188 | |
189 | // want one-based line numbers for human readers, so +1 |
190 | const uint64_t line = splitter.get_index()+1; |
191 | // LineSplitter already ignores empty lines |
192 | ai_assert(s.length()); |
193 | if (s[0] != '#') { |
194 | DefaultLogger::get()->warn(AddLineNumber("expected token \'#\'" ,line)); |
195 | ++splitter; |
196 | continue; |
197 | } |
198 | // --- |
199 | // extract id, entity class name and argument string, |
200 | // but don't create the actual object yet. |
201 | // --- |
202 | const std::string::size_type n0 = s.find_first_of('='); |
203 | if (n0 == std::string::npos) { |
204 | DefaultLogger::get()->warn(AddLineNumber("expected token \'=\'" ,line)); |
205 | ++splitter; |
206 | continue; |
207 | } |
208 | |
209 | const uint64_t id = strtoul10_64(s.substr(1,n0-1).c_str()); |
210 | if (!id) { |
211 | DefaultLogger::get()->warn(AddLineNumber("expected positive, numeric entity id" ,line)); |
212 | ++splitter; |
213 | continue; |
214 | } |
215 | std::string::size_type n1 = s.find_first_of('(',n0); |
216 | if (n1 == std::string::npos) { |
217 | has_next = true; |
218 | bool ok = false; |
219 | for( ++splitter; splitter; ++splitter) { |
220 | const std::string& snext = *splitter; |
221 | if (snext.empty()) { |
222 | continue; |
223 | } |
224 | |
225 | // the next line doesn't start an entity, so maybe it is |
226 | // just a continuation for this line, keep going |
227 | if (!IsEntityDef(snext)) { |
228 | s.append(snext); |
229 | n1 = s.find_first_of('(',n0); |
230 | ok = (n1 != std::string::npos); |
231 | } |
232 | else { |
233 | break; |
234 | } |
235 | } |
236 | |
237 | if(!ok) { |
238 | DefaultLogger::get()->warn(AddLineNumber("expected token \'(\'" ,line)); |
239 | continue; |
240 | } |
241 | } |
242 | |
243 | std::string::size_type n2 = s.find_last_of(')'); |
244 | if (n2 == std::string::npos || n2 < n1 || n2 == s.length() - 1 || s[n2 + 1] != ';') { |
245 | |
246 | has_next = true; |
247 | bool ok = false; |
248 | for( ++splitter; splitter; ++splitter) { |
249 | const std::string& snext = *splitter; |
250 | if (snext.empty()) { |
251 | continue; |
252 | } |
253 | // the next line doesn't start an entity, so maybe it is |
254 | // just a continuation for this line, keep going |
255 | if (!IsEntityDef(snext)) { |
256 | s.append(snext); |
257 | n2 = s.find_last_of(')'); |
258 | ok = !(n2 == std::string::npos || n2 < n1 || n2 == s.length() - 1 || s[n2 + 1] != ';'); |
259 | } |
260 | else { |
261 | break; |
262 | } |
263 | } |
264 | if(!ok) { |
265 | DefaultLogger::get()->warn(AddLineNumber("expected token \')\'" ,line)); |
266 | continue; |
267 | } |
268 | } |
269 | |
270 | if (map.find(id) != map.end()) { |
271 | DefaultLogger::get()->warn(AddLineNumber((Formatter::format(),"an object with the id #" ,id," already exists" ),line)); |
272 | } |
273 | |
274 | std::string::size_type ns = n0; |
275 | do ++ns; while( IsSpace(s.at(ns))); |
276 | std::string::size_type ne = n1; |
277 | do --ne; while( IsSpace(s.at(ne))); |
278 | std::string type = s.substr(ns,ne-ns+1); |
279 | std::transform( type.begin(), type.end(), type.begin(), &Assimp::ToLower<char> ); |
280 | const char* sz = scheme.GetStaticStringForToken(type); |
281 | if(sz) { |
282 | const std::string::size_type len = n2-n1+1; |
283 | char* const copysz = new char[len+1]; |
284 | std::copy(s.c_str()+n1,s.c_str()+n2+1,copysz); |
285 | copysz[len] = '\0'; |
286 | db.InternInsert(new LazyObject(db,id,line,sz,copysz)); |
287 | } |
288 | if(!has_next) { |
289 | ++splitter; |
290 | } |
291 | } |
292 | |
293 | if (!splitter) { |
294 | DefaultLogger::get()->warn("STEP: ignoring unexpected EOF" ); |
295 | } |
296 | |
297 | if ( !DefaultLogger::isNullLogger()){ |
298 | DefaultLogger::get()->debug((Formatter::format(),"STEP: got " ,map.size()," object records with " , |
299 | db.GetRefs().size()," inverse index entries" )); |
300 | } |
301 | } |
302 | |
303 | // ------------------------------------------------------------------------------------------------ |
304 | std::shared_ptr<const EXPRESS::DataType> EXPRESS::DataType::Parse(const char*& inout,uint64_t line, const EXPRESS::ConversionSchema* schema /*= NULL*/) |
305 | { |
306 | const char* cur = inout; |
307 | SkipSpaces(&cur); |
308 | if (*cur == ',' || IsSpaceOrNewLine(*cur)) { |
309 | throw STEP::SyntaxError("unexpected token, expected parameter" ,line); |
310 | } |
311 | |
312 | // just skip over constructions such as IFCPLANEANGLEMEASURE(0.01) and read only the value |
313 | if (schema) { |
314 | bool ok = false; |
315 | for(const char* t = cur; *t && *t != ')' && *t != ','; ++t) { |
316 | if (*t=='(') { |
317 | if (!ok) { |
318 | break; |
319 | } |
320 | for(--t;IsSpace(*t);--t); |
321 | std::string s(cur,static_cast<size_t>(t-cur+1)); |
322 | std::transform(s.begin(),s.end(),s.begin(),&ToLower<char> ); |
323 | if (schema->IsKnownToken(s)) { |
324 | for(cur = t+1;*cur++ != '(';); |
325 | const std::shared_ptr<const EXPRESS::DataType> dt = Parse(cur); |
326 | inout = *cur ? cur+1 : cur; |
327 | return dt; |
328 | } |
329 | break; |
330 | } |
331 | else if (!IsSpace(*t)) { |
332 | ok = true; |
333 | } |
334 | } |
335 | } |
336 | |
337 | if (*cur == '*' ) { |
338 | inout = cur+1; |
339 | return std::make_shared<EXPRESS::ISDERIVED>(); |
340 | } |
341 | else if (*cur == '$' ) { |
342 | inout = cur+1; |
343 | return std::make_shared<EXPRESS::UNSET>(); |
344 | } |
345 | else if (*cur == '(' ) { |
346 | // start of an aggregate, further parsing is done by the LIST factory constructor |
347 | inout = cur; |
348 | return EXPRESS::LIST::Parse(inout,line,schema); |
349 | } |
350 | else if (*cur == '.' ) { |
351 | // enum (includes boolean) |
352 | const char* start = ++cur; |
353 | for(;*cur != '.';++cur) { |
354 | if (*cur == '\0') { |
355 | throw STEP::SyntaxError("enum not closed" ,line); |
356 | } |
357 | } |
358 | inout = cur+1; |
359 | return std::make_shared<EXPRESS::ENUMERATION>(std::string(start, static_cast<size_t>(cur-start) )); |
360 | } |
361 | else if (*cur == '#' ) { |
362 | // object reference |
363 | return std::make_shared<EXPRESS::ENTITY>(strtoul10_64(++cur,&inout)); |
364 | } |
365 | else if (*cur == '\'' ) { |
366 | // string literal |
367 | const char* start = ++cur; |
368 | |
369 | for(;*cur != '\'';++cur) { |
370 | if (*cur == '\0') { |
371 | throw STEP::SyntaxError("string literal not closed" ,line); |
372 | } |
373 | } |
374 | |
375 | if (cur[1] == '\'') { |
376 | // Vesanen: more than 2 escaped ' in one literal! |
377 | do { |
378 | for(cur += 2;*cur != '\'';++cur) { |
379 | if (*cur == '\0') { |
380 | throw STEP::SyntaxError("string literal not closed" ,line); |
381 | } |
382 | } |
383 | } |
384 | while(cur[1] == '\''); |
385 | } |
386 | |
387 | inout = cur + 1; |
388 | |
389 | // assimp is supposed to output UTF8 strings, so we have to deal |
390 | // with foreign encodings. |
391 | std::string stemp = std::string(start, static_cast<size_t>(cur - start)); |
392 | if(!StringToUTF8(stemp)) { |
393 | // TODO: route this to a correct logger with line numbers etc., better error messages |
394 | DefaultLogger::get()->error("an error occurred reading escape sequences in ASCII text" ); |
395 | } |
396 | |
397 | return std::make_shared<EXPRESS::STRING>(stemp); |
398 | } |
399 | else if (*cur == '\"' ) { |
400 | throw STEP::SyntaxError("binary data not supported yet" ,line); |
401 | } |
402 | |
403 | // else -- must be a number. if there is a decimal dot in it, |
404 | // parse it as real value, otherwise as integer. |
405 | const char* start = cur; |
406 | for(;*cur && *cur != ',' && *cur != ')' && !IsSpace(*cur);++cur) { |
407 | if (*cur == '.') { |
408 | double f; |
409 | inout = fast_atoreal_move<double>(start,f); |
410 | return std::make_shared<EXPRESS::REAL>(f); |
411 | } |
412 | } |
413 | |
414 | bool neg = false; |
415 | if (*start == '-') { |
416 | neg = true; |
417 | ++start; |
418 | } |
419 | else if (*start == '+') { |
420 | ++start; |
421 | } |
422 | int64_t num = static_cast<int64_t>( strtoul10_64(start,&inout) ); |
423 | return std::make_shared<EXPRESS::INTEGER>(neg?-num:num); |
424 | } |
425 | |
426 | |
427 | // ------------------------------------------------------------------------------------------------ |
428 | std::shared_ptr<const EXPRESS::LIST> EXPRESS::LIST::Parse(const char*& inout,uint64_t line, const EXPRESS::ConversionSchema* schema /*= NULL*/) |
429 | { |
430 | const std::shared_ptr<EXPRESS::LIST> list = std::make_shared<EXPRESS::LIST>(); |
431 | EXPRESS::LIST::MemberList& members = list->members; |
432 | |
433 | const char* cur = inout; |
434 | if (*cur++ != '(') { |
435 | throw STEP::SyntaxError("unexpected token, expected \'(\' token at beginning of list" ,line); |
436 | } |
437 | |
438 | // estimate the number of items upfront - lists can grow large |
439 | size_t count = 1; |
440 | for(const char* c=cur; *c && *c != ')'; ++c) { |
441 | count += (*c == ',' ? 1 : 0); |
442 | } |
443 | |
444 | members.reserve(count); |
445 | |
446 | for(;;++cur) { |
447 | if (!*cur) { |
448 | throw STEP::SyntaxError("unexpected end of line while reading list" ); |
449 | } |
450 | SkipSpaces(cur,&cur); |
451 | if (*cur == ')') { |
452 | break; |
453 | } |
454 | |
455 | members.push_back( EXPRESS::DataType::Parse(cur,line,schema)); |
456 | SkipSpaces(cur,&cur); |
457 | |
458 | if (*cur != ',') { |
459 | if (*cur == ')') { |
460 | break; |
461 | } |
462 | throw STEP::SyntaxError("unexpected token, expected \',\' or \')\' token after list element" ,line); |
463 | } |
464 | } |
465 | |
466 | inout = cur+1; |
467 | return list; |
468 | } |
469 | |
470 | |
471 | // ------------------------------------------------------------------------------------------------ |
472 | STEP::LazyObject::LazyObject(DB& db, uint64_t id,uint64_t /*line*/, const char* const type,const char* args) |
473 | : id(id) |
474 | , type(type) |
475 | , db(db) |
476 | , args(args) |
477 | , obj() |
478 | { |
479 | // find any external references and store them in the database. |
480 | // this helps us emulate STEPs INVERSE fields. |
481 | if (db.KeepInverseIndicesForType(type)) { |
482 | const char* a = args; |
483 | |
484 | // do a quick scan through the argument tuple and watch out for entity references |
485 | int64_t skip_depth = 0; |
486 | while(*a) { |
487 | if (*a == '(') { |
488 | ++skip_depth; |
489 | } |
490 | else if (*a == ')') { |
491 | --skip_depth; |
492 | } |
493 | |
494 | if (skip_depth >= 1 && *a=='#') { |
495 | const char* tmp; |
496 | const int64_t num = static_cast<int64_t>( strtoul10_64(a+1,&tmp) ); |
497 | db.MarkRef(num,id); |
498 | } |
499 | ++a; |
500 | } |
501 | |
502 | } |
503 | } |
504 | |
505 | // ------------------------------------------------------------------------------------------------ |
506 | STEP::LazyObject::~LazyObject() |
507 | { |
508 | // make sure the right dtor/operator delete get called |
509 | if (obj) { |
510 | delete obj; |
511 | } |
512 | else delete[] args; |
513 | } |
514 | |
515 | // ------------------------------------------------------------------------------------------------ |
516 | void STEP::LazyObject::LazyInit() const |
517 | { |
518 | const EXPRESS::ConversionSchema& schema = db.GetSchema(); |
519 | STEP::ConvertObjectProc proc = schema.GetConverterProc(type); |
520 | |
521 | if (!proc) { |
522 | throw STEP::TypeError("unknown object type: " + std::string(type),id); |
523 | } |
524 | |
525 | const char* acopy = args; |
526 | std::shared_ptr<const EXPRESS::LIST> conv_args = EXPRESS::LIST::Parse(acopy,STEP::SyntaxError::LINE_NOT_SPECIFIED,&db.GetSchema()); |
527 | delete[] args; |
528 | args = NULL; |
529 | |
530 | // if the converter fails, it should throw an exception, but it should never return NULL |
531 | try { |
532 | obj = proc(db,*conv_args); |
533 | } |
534 | catch(const TypeError& t) { |
535 | // augment line and entity information |
536 | throw TypeError(t.what(),id); |
537 | } |
538 | ++db.evaluated_count; |
539 | ai_assert(obj); |
540 | |
541 | // store the original id in the object instance |
542 | obj->SetID(id); |
543 | } |
544 | |
545 | |