KiCad PCB EDA Suite
Loading...
Searching...
No Matches
easypc_archive.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <algorithm>
23#include <cstring>
24
25#include <boost/endian/conversion.hpp>
26
27#include <ki_exception.h>
28#include <wx/debug.h>
29#include <wx/strconv.h>
30#include <wx/translation.h>
31
32
33namespace EASYPC
34{
35
36namespace
37{
38
39 constexpr uint16_t NEW_CLASS_TAG = 0xFFFF;
40 constexpr uint16_t CLASS_TAG = 0x8000;
41 constexpr uint32_t BIG_CLASS_TAG = 0x80000000;
42 constexpr uint16_t BIG_OBJECT_TAG = 0x7FFF;
43
44 // Class names are shorter than 64 bytes
45 constexpr uint16_t MAX_CLASS_NAME = 64;
46
47
48 const REGISTRY& registry()
49 {
50 static const REGISTRY reg = []()
51 {
52 REGISTRY r;
58 return r;
59 }();
60
61 return reg;
62 }
63
64
65 LOAD_TASK readRoot( ARCHIVE& aAr, OBJECT*& aResult )
66 {
67 aResult = co_await aAr.Object();
68 }
69
70
71 LOAD_TASK embedRoot( ARCHIVE& aAr, OBJECT& aObject, const char* aClass )
72 {
73 co_await aAr.Embedded( aObject, aClass );
74 }
75
76} // namespace
77
78
80{
81 co_return;
82}
83
84
85int OBJECT::ItemFormat( const ARCHIVE& aAr ) const
86{
87 return aAr.CurrentReadFormat();
88}
89
90
91ARCHIVE::ARCHIVE( const uint8_t* aData, size_t aSize, DOC_KIND aKind, int32_t aProduct ) :
92 m_data( aData ),
93 m_size( aSize ),
94 m_kind( aKind ),
95 m_product( aProduct )
96{
97 m_load.emplace_back();
98}
99
100
101void ARCHIVE::Require( size_t aLen ) const
102{
103 if( aLen > m_size - m_pos )
104 {
105 THROW_IO_ERROR( wxString::Format( _( "Easy-PC archive ends at offset %zu, %zu bytes short of a %zu byte "
106 "read." ),
107 m_pos, aLen - ( m_size - m_pos ), aLen ) );
108 }
109}
110
111
112void ARCHIVE::Skip( size_t aLen )
113{
114 Require( aLen );
115 m_pos += aLen;
116}
117
118
119uint8_t ARCHIVE::U8()
120{
121 Require( 1 );
122 return m_data[m_pos++];
123}
124
125
127{
128 return m_boolIsByte ? U8() != 0 : U32() != 0;
129}
130
131
132uint16_t ARCHIVE::U16()
133{
134 Require( 2 );
135 m_pos += 2;
136 return boost::endian::load_little_u16( m_data + m_pos - 2 );
137}
138
139
140uint32_t ARCHIVE::U32()
141{
142 Require( 4 );
143 m_pos += 4;
144 return boost::endian::load_little_u32( m_data + m_pos - 4 );
145}
146
147
148uint64_t ARCHIVE::U64()
149{
150 Require( 8 );
151 m_pos += 8;
152 return boost::endian::load_little_u64( m_data + m_pos - 8 );
153}
154
155
157{
158 uint64_t bits = U64();
159 double v;
160 std::memcpy( &v, &bits, sizeof( v ) );
161 return v;
162}
163
164
166{
167 uint16_t n = U16();
168 return n != 0xFFFF ? n : U32();
169}
170
171
172uint32_t ARCHIVE::stringLength( bool& aUnicode )
173{
174 // The length is a byte; 0xFF escapes to a WORD, whose 0xFFFE marks UTF-16 and restarts the length
175 aUnicode = false;
176 uint32_t len = U8();
177
178 if( len == 0xFF )
179 {
180 len = U16();
181
182 if( len == 0xFFFE )
183 {
184 aUnicode = true;
185 len = U8();
186
187 if( len == 0xFF )
188 len = U16();
189 }
190
191 if( len == 0xFFFF )
192 len = U32();
193 }
194
195 return len;
196}
197
198
200{
201 bool unicode;
202 uint32_t len = stringLength( unicode );
203 size_t bytes = unicode ? static_cast<size_t>( len ) * 2 : len;
204
205 Require( bytes );
206
207 const char* text = reinterpret_cast<const char*>( m_data + m_pos );
208 m_pos += bytes;
209
210 if( unicode )
211 return wxString( text, wxMBConvUTF16LE(), bytes );
212
213 static const wxCSConv cp1252( wxFONTENCODING_CP1252 );
214 return wxString( text, cp1252, bytes );
215}
216
217
219{
220 bool unicode;
221 uint32_t len = stringLength( unicode );
222 Skip( unicode ? static_cast<size_t>( len ) * 2 : len );
223}
224
225
226OBJECT* ARCHIVE::readTag( bool& aIsNew )
227{
228 aIsNew = false;
229
230 const size_t tag_pos = m_pos;
231 const uint16_t short_tag = U16();
232 uint32_t tag = short_tag;
233
234 if( short_tag == BIG_OBJECT_TAG )
235 tag = U32();
236 else if( short_tag & CLASS_TAG )
237 tag = BIG_CLASS_TAG | ( short_tag & ~CLASS_TAG );
238
239 const char* class_name = nullptr;
240 uint16_t class_schema = 0;
241
242 if( short_tag == NEW_CLASS_TAG )
243 {
244 class_schema = U16();
245 uint16_t name_length = U16();
246
247 if( name_length >= MAX_CLASS_NAME )
249 wxString::Format( _( "Easy-PC class name of %u bytes at offset %zu." ), name_length, tag_pos ) );
250
251 Require( name_length );
252 std::string_view name( reinterpret_cast<const char*>( m_data + m_pos ), name_length );
253 m_pos += name_length;
254
255 auto it = registry().find( name );
256
257 if( it == registry().end() )
258 {
259 THROW_IO_ERROR( wxString::Format( _( "Unsupported Easy-PC object '%s' at offset %zu." ),
260 wxString::FromUTF8( name.data(), name.size() ), tag_pos ) );
261 }
262
263 class_name = it->first.c_str();
264 m_load.push_back( SLOT{ class_name, class_schema, nullptr } );
265 }
266 else if( tag & BIG_CLASS_TAG )
267 {
268 uint32_t index = tag & ~BIG_CLASS_TAG;
269
270 if( index >= m_load.size() || !m_load[index].ClassName || m_load[index].Object )
271 {
272 THROW_IO_ERROR( wxString::Format( _( "Easy-PC class reference to slot %u at offset %zu is not a "
273 "class." ),
274 index, tag_pos ) );
275 }
276
277 class_name = m_load[index].ClassName;
278 class_schema = m_load[index].Schema;
279 }
280 else
281 {
282 if( tag == 0 )
283 return nullptr;
284
285 if( tag >= m_load.size() || !m_load[tag].Object )
286 {
287 THROW_IO_ERROR( wxString::Format( _( "Easy-PC object reference to slot %u at offset %zu is not an "
288 "object." ),
289 tag, tag_pos ) );
290 }
291
292 return m_load[tag].Object;
293 }
294
295 // Nested objects are heap frames, so this only bounds what a crafted stream can allocate
296 if( m_depth + 1 > MAX_DEPTH )
297 {
298 THROW_IO_ERROR( wxString::Format( _( "Easy-PC archive nests deeper than %d objects at offset %zu." ), MAX_DEPTH,
299 tag_pos ) );
300 }
301
302 std::unique_ptr<OBJECT> created = registry().find( class_name )->second();
303 OBJECT* obj = created.get();
304 obj->ClassName = class_name;
305 obj->Schema = class_schema;
306 obj->Offset = tag_pos;
307
308 // The new object takes its slot before its fields, so references to it from inside resolve
309 m_load.push_back( SLOT{ class_name, class_schema, obj } );
310 m_objects.push_back( std::move( created ) );
311
312 aIsNew = true;
313 return obj;
314}
315
316
318{
319 bool isNew = false;
320 m_object = m_archive.readTag( isNew );
321 return !isNew;
322}
323
324
325void ARCHIVE::OBJECT_AWAITER::await_suspend( std::coroutine_handle<> aAwaiting )
326{
327 m_archive.pushObjectLoad( *m_object, m_object->Schema );
328}
329
330
331void ARCHIVE::EMBEDDED_AWAITER::await_suspend( std::coroutine_handle<> aAwaiting )
332{
333 m_object.Offset = m_archive.m_pos;
334
335 // An embedded member keeps the schema of the object around it
336 m_archive.pushObjectLoad( m_object, m_archive.m_objectSchema );
337}
338
339
340void LOAD_TASK::await_suspend( std::coroutine_handle<> aAwaiting )
341{
342 m_handle.promise().m_archive->pushTask( Release() );
343}
344
345
346void ARCHIVE::pushObjectLoad( OBJECT& aObject, uint16_t aSchema )
347{
348 if( m_depth + 1 > MAX_DEPTH )
349 {
350 THROW_IO_ERROR( wxString::Format( _( "Easy-PC archive nests deeper than %d objects at offset %zu." ), MAX_DEPTH,
351 m_pos ) );
352 }
353
354 m_frames.push_back( FRAME{ aObject.Load( *this ).Release(), &aObject, m_objectSchema, true } );
355 m_objectSchema = aSchema;
356 ++m_depth;
357}
358
359
361{
362 OBJECT* owner = m_frames.empty() ? nullptr : m_frames.back().Object;
363 m_frames.push_back( FRAME{ aHandle, owner, m_objectSchema, false } );
364}
365
366
368{
369 const size_t base = m_frames.size();
370 pushTask( aRoot.Release() );
371
372 auto pop = [&]()
373 {
374 FRAME frame = m_frames.back();
375 m_frames.pop_back();
376 frame.Handle.destroy();
378
379 if( frame.ObjectLoad )
380 --m_depth;
381 };
382
383 while( m_frames.size() > base )
384 {
385 // Resuming may push frames, so the top is fetched afresh each time round
386 LOAD_TASK::HANDLE top = m_frames.back().Handle;
387
388 if( !top.done() )
389 {
390 top.resume();
391 continue;
392 }
393
394 std::exception_ptr error = top.promise().m_error;
395
396 if( !error )
397 {
398 pop();
399 continue;
400 }
401
402 // An embedded owner lives in a loader's frame, so it is described before the frames are destroyed
403 const OBJECT* owner = m_frames.back().Object;
404 wxString ownerName = owner ? wxString::FromUTF8( owner->ClassName ) : wxString();
405 size_t ownerOffset = owner ? owner->Offset : 0;
406
407 while( m_frames.size() > base )
408 pop();
409
410 try
411 {
412 std::rethrow_exception( error );
413 }
414 catch( const IO_ERROR& e )
415 {
416 // Only the innermost object is named
417 if( m_errorHasContext || !owner )
418 throw;
419
420 m_errorHasContext = true;
421 THROW_IO_ERROR( wxString::Format( wxS( "%s at offset 0x%zX: %s" ), ownerName, ownerOffset, e.Problem() ) );
422 }
423 }
424}
425
426
428{
429 OBJECT* result = nullptr;
430 drive( readRoot( *this, result ) );
431 return result;
432}
433
434
435void ARCHIVE::LoadEmbedded( OBJECT& aObject, const char* aClass )
436{
437 drive( embedRoot( *this, aObject, aClass ) );
438}
439
440
442{
443 m_load.push_back( SLOT{ aObject ? aObject->ClassName : nullptr, 0, aObject } );
444}
445
446
447std::unique_ptr<OBJECT> ARCHIVE::Create( const char* aClass )
448{
449 auto it = registry().find( std::string_view( aClass ) );
450
451 if( it == registry().end() )
452 THROW_IO_ERROR( wxString::Format( _( "Easy-PC class '%s' is not registered." ), aClass ) );
453
454 std::unique_ptr<OBJECT> obj = it->second();
455 obj->ClassName = it->first.c_str();
456 return obj;
457}
458
459
460LOAD_TASK SkipFields( ARCHIVE& aAr, std::string_view aOps )
461{
462 for( size_t i = 0; i < aOps.size(); )
463 {
464 char op = aOps[i++];
465 size_t count = 0;
466
467 // A space separates ops; read as one it would take a following '1' op as its count
468 if( op == ' ' )
469 continue;
470
471 for( ; i < aOps.size() && aOps[i] >= '0' && aOps[i] <= '9'; ++i )
472 count = count * 10 + ( aOps[i] - '0' );
473
474 for( count = std::max<size_t>( count, 1 ); count > 0; --count )
475 {
476 switch( op )
477 {
478 case 'b': aAr.InBool(); break;
479 case '1': aAr.Skip( 1 ); break;
480 case 'i': aAr.Skip( 4 ); break;
481 case 'f': aAr.Skip( 8 ); break;
482 case 's': aAr.SkipString(); break;
483 case 'o': co_await aAr.Object(); break;
484 case 'a': aAr.Skip( 4 * size_t( aAr.Count() ) ); break;
485 default: wxFAIL_MSG( wxS( "Bad SkipFields op" ) );
486 }
487 }
488 }
489}
490
491
492LOAD_TASK SkipRows( ARCHIVE& aAr, int aVersion, std::span<const FIELD_ROW> aRows )
493{
494 for( const FIELD_ROW& row : aRows )
495 {
496 if( aVersion >= row.Since && aVersion <= row.Until )
497 co_await SkipFields( aAr, row.Ops );
498 }
499}
500
501
503{
504 // Only a design has a document for a null parent to mean
506}
507
508
509void ARCHIVE::throwBadClass( const OBJECT& aObject ) const
510{
511 THROW_IO_ERROR( wxString::Format( _( "Easy-PC object '%s' at offset %zu is not of the class required there." ),
512 aObject.ClassName, aObject.Offset ) );
513}
514
515} // namespace EASYPC
int index
const char * name
void await_suspend(std::coroutine_handle<> aAwaiting)
void await_suspend(std::coroutine_handle<> aAwaiting)
One archive load stream; it owns every object it creates.
void Skip(size_t aLen)
void pushTask(LOAD_TASK::HANDLE aHandle)
friend class LOAD_TASK
int CurrentReadFormat() const
OBJECT * readTag(bool &aIsNew)
Read a tag; a new object is created and mapped but not loaded (aIsNew)
void MapObject(OBJECT *aObject)
A load-array slot for an object not read through a tag.
EMBEDDED_AWAITER Embedded(OBJECT &aObject, const char *aClass)
bool IsDesign() const
static std::unique_ptr< OBJECT > Create(const char *aClass)
An object of a registered class, not yet loaded.
void drive(LOAD_TASK aRoot)
Run aRoot and everything it awaits until it finishes, rethrowing its failure.
void Require(size_t aLen) const
Throw unless aLen more bytes remain.
wxString ReadString()
ANSI (windows-1252) or the 0xFF 0xFFFE UTF-16 form.
void pushObjectLoad(OBJECT &aObject, uint16_t aSchema)
uint32_t stringLength(bool &aUnicode)
The string length prefix; aUnicode for the UTF-16 form.
static constexpr int MAX_DEPTH
const uint8_t * m_data
uint16_t m_objectSchema
std::vector< std::unique_ptr< OBJECT > > m_objects
void throwBadClass(const OBJECT &aObject) const
bool InBool()
one or four bytes, depending on the active format
int OrphanFormat() const
A null parent tag is the document: the header version in a design, else the current read format.
std::vector< SLOT > m_load
slot 0 is the null slot
std::vector< FRAME > m_frames
OBJECT * ReadObject()
ReadObject and LoadEmbedded outside any loader, running the nested loads to completion.
uint32_t Count()
a 16-bit count, 0xFFFF escaping to 32 bits
OBJECT_AWAITER Object()
void LoadEmbedded(OBJECT &aObject, const char *aClass)
ARCHIVE(const uint8_t *aData, size_t aSize, DOC_KIND aKind, int32_t aProduct)
aData must outlive the archive; aProduct is the product key in force
A loader coroutine. Awaiting one queues it on its archive's frame stack instead of nesting the call.
void await_suspend(std::coroutine_handle<> aAwaiting)
std::coroutine_handle< promise_type > HANDLE
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString Problem() const
what was the problem?
Reader for the serialized streams of Easy-PC and DesignSpark PCB.
#define _(s)
#define THROW_IO_ERROR(msg)
macro which captures the "call site" values of FILE_, __FUNCTION & LINE
std::map< std::string, std::function< std::unique_ptr< OBJECT >()>, std::less<> > REGISTRY
LOAD_TASK SkipRows(ARCHIVE &aAr, int aVersion, std::span< const FIELD_ROW > aRows)
SkipFields for every row aVersion falls in, in order.
void RegisterGeometryClasses(REGISTRY &aReg)
void RegisterLibraryClasses(REGISTRY &aReg)
LOAD_TASK SkipFields(ARCHIVE &aAr, std::string_view aOps)
Read and drop a run of fields described by aOps, each letter optionally followed by a repeat count: b...
DOC_KIND
Document kind, from the FileType stream or the file extension for projects.
void RegisterRootClasses(REGISTRY &aReg)
void RegisterStyleClasses(REGISTRY &aReg)
void RegisterConnectivityClasses(REGISTRY &aReg)
LOAD_TASK::HANDLE Handle
bool ObjectLoad
counts towards MAX_DEPTH
A run of fields present from version since to until inclusive.
Root of every deserialized object.
virtual LOAD_TASK Load(ARCHIVE &aAr)
Read this object's fields.
size_t Offset
stream offset of the tag or of an embedded member's first field
uint16_t Schema
object schema from the class tag, 0 for an embedded member
const char * ClassName
registered class name
virtual int ItemFormat(const ARCHIVE &aAr) const
The version this object gates on; the base is the archive's current read format.
KIBIS top(path, &reporter)
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.