Coverage Report

Created: 2026-03-12 06:25

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