KiCad PCB EDA Suite
Loading...
Searching...
No Matches
easyedapro_v3_parser.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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <algorithm>
27#include <cmath>
28#include <optional>
29#include <set>
30
32
34#include <json_common.h>
35#include <ki_exception.h>
36
37#include <wx/filename.h>
38#include <wx/log.h>
39#include <wx/mstream.h>
40#include <wx/txtstrm.h>
41#include <wx/wfstream.h>
42#include <wx/zipstrm.h>
43
44
47
48
49static std::string ToStdString( const wxString& aStr )
50{
51 return std::string( aStr.ToUTF8() );
52}
53
54
55namespace EASYEDAPRO
56{
57
58wxString V3JsonToString( const nlohmann::json& aValue, const wxString& aDefault )
59{
60 if( aValue.is_string() )
61 return aValue.get<wxString>();
62
63 if( aValue.is_number_integer() )
64 return wxString::Format( "%lld", static_cast<long long>( aValue.get<int64_t>() ) );
65
66 if( aValue.is_number_unsigned() )
67 return wxString::Format( "%llu", static_cast<unsigned long long>( aValue.get<uint64_t>() ) );
68
69 if( aValue.is_number_float() )
70 return wxString::FromDouble( aValue.get<double>() );
71
72 if( aValue.is_boolean() )
73 return aValue.get<bool>() ? wxS( "true" ) : wxS( "false" );
74
75 return aDefault;
76}
77
78
79wxString V3GetString( const nlohmann::json& aObj, const char* aKey, const wxString& aDefault )
80{
81 if( !aObj.is_object() )
82 return aDefault;
83
84 auto it = aObj.find( aKey );
85
86 if( it == aObj.end() )
87 return aDefault;
88
89 return V3JsonToString( *it, aDefault );
90}
91
92
93wxString V3GetString( const nlohmann::json& aObj, const wxString& aKey, const wxString& aDefault )
94{
95 return V3GetString( aObj, ToStdString( aKey ).c_str(), aDefault );
96}
97
98
99double V3GetDouble( const nlohmann::json& aObj, const char* aKey, double aDefault )
100{
101 if( !aObj.is_object() )
102 return aDefault;
103
104 auto it = aObj.find( aKey );
105
106 if( it == aObj.end() )
107 return aDefault;
108
109 if( it->is_number() )
110 return it->get<double>();
111
112 if( it->is_string() )
113 {
114 double value;
115
116 if( it->get<wxString>().ToCDouble( &value ) )
117 return value;
118 }
119
120 return aDefault;
121}
122
123
124int V3GetInt( const nlohmann::json& aObj, const char* aKey, int aDefault )
125{
126 if( !aObj.is_object() )
127 return aDefault;
128
129 auto it = aObj.find( aKey );
130
131 if( it == aObj.end() )
132 return aDefault;
133
134 if( it->is_number_integer() )
135 return it->get<int>();
136
137 if( it->is_number_float() )
138 return static_cast<int>( std::lround( it->get<double>() ) );
139
140 if( it->is_boolean() )
141 return it->get<bool>() ? 1 : 0;
142
143 if( it->is_string() )
144 {
145 long value = 0;
146
147 if( it->get<wxString>().ToLong( &value ) )
148 return static_cast<int>( value );
149 }
150
151 return aDefault;
152}
153
154
155bool V3GetBool( const nlohmann::json& aObj, const char* aKey, bool aDefault )
156{
157 if( !aObj.is_object() )
158 return aDefault;
159
160 auto it = aObj.find( aKey );
161
162 if( it == aObj.end() )
163 return aDefault;
164
165 if( it->is_boolean() )
166 return it->get<bool>();
167
168 if( it->is_number() )
169 return it->get<double>() != 0.0;
170
171 if( it->is_string() )
172 {
173 wxString s = it->get<wxString>();
174
175 if( s.CmpNoCase( wxS( "true" ) ) == 0 )
176 return true;
177
178 if( s.CmpNoCase( wxS( "false" ) ) == 0 )
179 return false;
180
181 long value = 0;
182
183 if( s.ToLong( &value ) )
184 return value != 0;
185 }
186
187 return aDefault;
188}
189
190
191bool V3IsNullOrMissing( const nlohmann::json& aObj, const char* aKey )
192{
193 if( !aObj.is_object() )
194 return true;
195
196 auto it = aObj.find( aKey );
197
198 return it == aObj.end() || it->is_null();
199}
200
201
202nlohmann::json V3ParseIdArray( const wxString& aId )
203{
204 if( aId.empty() )
205 return nlohmann::json();
206
207 try
208 {
209 return nlohmann::json::parse( ToStdString( aId ) );
210 }
211 catch( ... )
212 {
213 return nlohmann::json();
214 }
215}
216
217} // namespace EASYEDAPRO
218
219
220namespace
221{
222
223// Helper functions for internal use
224static inline wxString GetString( const nlohmann::json& aObj, const char* aKey,
225 const wxString& aDefault = wxEmptyString )
226{
227 return EASYEDAPRO::V3GetString( aObj, aKey, aDefault );
228}
229
230static inline int GetInt( const nlohmann::json& aObj, const char* aKey, int aDefault = 0 )
231{
232 return EASYEDAPRO::V3GetInt( aObj, aKey, aDefault );
233}
234
235
236static wxString ParseDocType( const nlohmann::json& aHead )
237{
238 return GetString( aHead, "docType" );
239}
240
241
242static wxString ParseDocUuid( const nlohmann::json& aHead )
243{
244 return GetString( aHead, "uuid" );
245}
246
247
248static void ParseEpruStream( wxInputStream& aInput, const wxString& aSource,
249 std::vector<V3_DOC_RAW>& aDocs )
250{
251 wxTextInputStream txt( aInput, wxS( " " ), wxConvUTF8 );
252
253 int currentLine = 1;
254 std::optional<V3_DOC_RAW> currentDoc;
255
256 auto flushDoc = [&]()
257 {
258 if( currentDoc )
259 {
260 aDocs.push_back( std::move( *currentDoc ) );
261 currentDoc.reset();
262 }
263 };
264
265 while( aInput.CanRead() )
266 {
267 wxString rawLine = txt.ReadLine();
268
269 if( rawLine.empty() )
270 {
271 currentLine++;
272 continue;
273 }
274
275 if( rawLine.Last() == '|' )
276 rawLine.RemoveLast();
277
278 int sepPos = rawLine.Find( wxS( "||" ) );
279
280 if( sepPos < 0 )
281 {
282 currentLine++;
283 continue;
284 }
285
286 wxString outerStr = rawLine.Left( sepPos );
287 wxString innerStr = rawLine.Mid( sepPos + 2 );
288
289 try
290 {
291 nlohmann::json outer = nlohmann::json::parse( ToStdString( outerStr ) );
292 nlohmann::json inner;
293
294 if( !innerStr.empty() )
295 inner = nlohmann::json::parse( ToStdString( innerStr ) );
296
297 wxString type = GetString( outer, "type" );
298
299 if( type == wxS( "DOCHEAD" ) )
300 {
301 flushDoc();
302
303 V3_DOC_RAW doc;
304 doc.head = inner;
305 doc.docType = ParseDocType( inner );
306 doc.uuid = ParseDocUuid( inner );
307
308 if( doc.docType.empty() || doc.uuid.empty() )
309 THROW_IO_ERRORF( _( "Invalid DOCHEAD in '%s' at line %d" ), aSource, currentLine );
310
311 currentDoc = std::move( doc );
312 currentLine++;
313 continue;
314 }
315
316 if( !currentDoc )
317 THROW_IO_ERRORF( _( "Expected DOCHEAD in '%s' at line %d" ), aSource, currentLine );
318
319 V3_ROW row;
320 row.outer = std::move( outer );
321 row.inner = std::move( inner );
322 row.type = type;
323 row.id = GetString( row.outer, "id" );
324 row.ticket = GetInt( row.outer, "ticket", 0 );
325
326 wxString dedupId = row.id;
327
328 if( dedupId.empty() )
329 dedupId = wxString::Format( "__line_%d", currentLine );
330
331 auto it = currentDoc->rowById.find( dedupId );
332
333 if( it == currentDoc->rowById.end() )
334 {
335 currentDoc->rowById[dedupId] = currentDoc->rows.size();
336 currentDoc->rows.emplace_back( std::move( row ) );
337 }
338 else if( row.ticket >= currentDoc->rows[it->second].ticket )
339 {
340 currentDoc->rows[it->second] = std::move( row );
341 }
342 }
343 catch( nlohmann::json::exception& e )
344 {
345 THROW_IO_ERRORF( _( "JSON error in '%s' at line %d: %s" ), aSource, currentLine, e.what() );
346 }
347
348 currentLine++;
349 }
350
351 flushDoc();
352}
353
354
355struct V3_LIBRARY_CONTENTS
356{
357 bool hasIndex = false;
358 bool hasElibu = false;
359 std::set<wxString> docTypes;
360};
361
362
363static bool HasValidV3LibraryContents( const V3_LIBRARY_CONTENTS& aContents, const wxString& aRequiredDocType )
364{
365 if( !aContents.hasIndex || !aContents.hasElibu )
366 return false;
367
368 if( aRequiredDocType.empty() )
369 return true;
370
371 return aContents.docTypes.contains( aRequiredDocType );
372}
373
374
375static void ScanEpruDocTypes( wxInputStream& aInput, const wxString& aSource, std::set<wxString>& aDocTypes )
376{
377 wxTextInputStream txt( aInput, wxS( " " ), wxConvUTF8 );
378 int currentLine = 1;
379
380 while( aInput.CanRead() )
381 {
382 wxString rawLine = txt.ReadLine();
383
384 if( rawLine.empty() )
385 {
386 currentLine++;
387 continue;
388 }
389
390 if( rawLine.Last() == '|' )
391 rawLine.RemoveLast();
392
393 int sepPos = rawLine.Find( wxS( "||" ) );
394
395 if( sepPos < 0 )
396 {
397 currentLine++;
398 continue;
399 }
400
401 wxString outerStr = rawLine.Left( sepPos );
402 wxString innerStr = rawLine.Mid( sepPos + 2 );
403
404 try
405 {
406 nlohmann::json outer = nlohmann::json::parse( ToStdString( outerStr ) );
407
408 if( GetString( outer, "type" ) == wxS( "DOCHEAD" ) && !innerStr.empty() )
409 {
410 nlohmann::json inner = nlohmann::json::parse( ToStdString( innerStr ) );
411 wxString docType = ParseDocType( inner );
412
413 if( !docType.empty() )
414 aDocTypes.insert( docType );
415 }
416 }
417 catch( nlohmann::json::exception& e )
418 {
419 THROW_IO_ERRORF( _( "JSON error in '%s' at line %d: %s" ), aSource, currentLine, e.what() );
420 }
421
422 currentLine++;
423 }
424}
425
426
427static nlohmann::json ReadJsonEntry( wxInputStream& aInput, const wxString& aEntryName )
428{
429 wxMemoryOutputStream mem;
430 mem << aInput;
431
432 wxStreamBuffer* buf = mem.GetOutputStreamBuffer();
433 std::string jsonStr( static_cast<const char*>( buf->GetBufferStart() ), buf->GetBufferSize() );
434
435 try
436 {
437 return nlohmann::json::parse( jsonStr );
438 }
439 catch( nlohmann::json::exception& e )
440 {
441 THROW_IO_ERRORF( _( "JSON error reading '%s': %s" ), aEntryName, e.what() );
442 }
443}
444
445
446static void MergeLibraryIndex( nlohmann::json& aTarget, nlohmann::json&& aIndex )
447{
448 if( !aIndex.is_object() )
449 return;
450
451 for( auto& [key, value] : aIndex.items() )
452 {
453 if( aTarget.contains( key ) && aTarget[key].is_object() && value.is_object() )
454 {
455 for( auto& [itemKey, itemValue] : value.items() )
456 aTarget[key][itemKey] = std::move( itemValue );
457 }
458 else if( !aTarget.contains( key ) || !value.is_object() || !value.empty() )
459 {
460 aTarget[key] = std::move( value );
461 }
462 }
463}
464
465
466static V3_LIBRARY_CONTENTS ScanV3LibraryArchive( const wxString& aFileName, const wxString& aRequiredDocType )
467{
468 V3_LIBRARY_CONTENTS contents;
469
470 std::shared_ptr<wxZipEntry> entry;
471 wxFFileInputStream in( aFileName );
472 wxZipInputStream zip( in );
473
474 if( !zip.IsOk() )
475 THROW_IO_ERRORF( _( "Cannot read ZIP archive '%s'" ), aFileName );
476
477 while( entry.reset( zip.GetNextEntry() ), entry.get() )
478 {
479 wxString entryName = entry->GetName();
480 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' ).Lower();
481
482 if( baseName == wxS( "symbol2.json" ) || baseName == wxS( "footprint2.json" )
483 || baseName == wxS( "device2.json" ) )
484 {
485 contents.hasIndex = true;
486 }
487 else if( baseName.EndsWith( wxS( ".elibu" ) ) )
488 {
489 contents.hasElibu = true;
490
491 if( !aRequiredDocType.empty() )
492 ScanEpruDocTypes( zip, entryName, contents.docTypes );
493 }
494
495 if( HasValidV3LibraryContents( contents, aRequiredDocType ) )
496 break;
497 }
498
499 return contents;
500}
501
502
503static std::vector<V3_DOC_RAW> ParseV3DocsFromArchive( const wxString& aFileName, nlohmann::json& aProject2 )
504{
505 std::vector<V3_DOC_RAW> docs;
506
507 bool hasProject2 = false;
508 bool hasEpru = false;
509
510 std::shared_ptr<wxZipEntry> entry;
511 wxFFileInputStream in( aFileName );
512 wxZipInputStream zip( in );
513
514 if( !zip.IsOk() )
515 THROW_IO_ERRORF( _( "Cannot read ZIP archive '%s'" ), aFileName );
516
517 while( entry.reset( zip.GetNextEntry() ), entry.get() )
518 {
519 wxString entryName = entry->GetName();
520 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
521
522 if( baseName.CmpNoCase( wxS( "project2.json" ) ) == 0 )
523 {
524 aProject2 = ReadJsonEntry( zip, entryName );
525 hasProject2 = true;
526 }
527 else if( baseName.Lower().EndsWith( wxS( ".epru" ) ) )
528 {
529 hasEpru = true;
530 ParseEpruStream( zip, entryName, docs );
531 }
532 }
533
534 if( !hasProject2 || !hasEpru )
535 {
536 THROW_IO_ERRORF( _( "'%s' does not appear to be a valid EasyEDA (JLCEDA) Pro v3 project. "
537 "Cannot find project2.json and .epru documents." ), aFileName );
538 }
539
540 return docs;
541}
542
543
544static std::vector<V3_DOC_RAW> ParseV3DocsFromLibraryArchive( const wxString& aFileName, nlohmann::json& aLibraryIndex )
545{
546 std::vector<V3_DOC_RAW> docs;
547 V3_LIBRARY_CONTENTS contents;
548
549 std::shared_ptr<wxZipEntry> entry;
550 wxFFileInputStream in( aFileName );
551 wxZipInputStream zip( in );
552
553 if( !zip.IsOk() )
554 THROW_IO_ERRORF( _( "Cannot read ZIP archive '%s'" ), aFileName );
555
556 aLibraryIndex = nlohmann::json::object();
557
558 while( entry.reset( zip.GetNextEntry() ), entry.get() )
559 {
560 wxString entryName = entry->GetName();
561 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
562 wxString lowerBaseName = baseName.Lower();
563
564 if( lowerBaseName == wxS( "symbol2.json" ) || lowerBaseName == wxS( "footprint2.json" )
565 || lowerBaseName == wxS( "device2.json" ) )
566 {
567 nlohmann::json index = ReadJsonEntry( zip, entryName );
568
569 MergeLibraryIndex( aLibraryIndex, std::move( index ) );
570
571 contents.hasIndex = true;
572 }
573 else if( lowerBaseName.EndsWith( wxS( ".elibu" ) ) )
574 {
575 contents.hasElibu = true;
576 ParseEpruStream( zip, entryName, docs );
577
578 for( const V3_DOC_RAW& doc : docs )
579 contents.docTypes.insert( doc.docType );
580 }
581 }
582
583 if( !HasValidV3LibraryContents( contents, wxEmptyString ) )
584 {
585 THROW_IO_ERRORF( _( "'%s' does not appear to be a valid EasyEDA (JLCEDA) Pro v3 library. Cannot find "
586 "symbol2.json, footprint2.json or device2.json and .elibu documents." ),
587 aFileName );
588 }
589
590 return docs;
591}
592
593
594} // namespace
595
596
597namespace EASYEDAPRO
598{
599
600V3_DOC_PARSER::V3_DOC_PARSER( const wxString& aFileName ) :
601 m_fileName( aFileName )
602{
603}
604
605
606bool V3_DOC_PARSER::IsV3Archive( const wxString& aFileName )
607{
608 wxFileName fn( aFileName );
609 wxString ext = fn.GetExt().Lower();
610
611 if( ext != wxS( "epro2" ) && ext != wxS( "zip" ) )
612 return false;
613
614 try
615 {
616 std::shared_ptr<wxZipEntry> entry;
617 wxFFileInputStream in( aFileName );
618 wxZipInputStream zip( in );
619
620 if( !zip.IsOk() )
621 return false;
622
623 bool hasProject2 = false;
624 bool hasEpru = false;
625
626 while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
627 {
628 wxString entryName = entry->GetName();
629 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
630
631 if( baseName.CmpNoCase( wxS( "project2.json" ) ) == 0 )
632 hasProject2 = true;
633 else if( baseName.Lower().EndsWith( wxS( ".epru" ) ) )
634 hasEpru = true;
635
636 if( hasProject2 && hasEpru )
637 return true;
638 }
639
640 return false;
641 }
642 catch( ... )
643 {
644 return false;
645 }
646}
647
648
649bool V3_DOC_PARSER::IsV3Library( const wxString& aFileName, const wxString& aRequiredDocType )
650{
651 wxFileName fn( aFileName );
652
653 if( fn.GetExt().Lower() != wxS( "elibz2" ) )
654 return false;
655
656 try
657 {
658 V3_LIBRARY_CONTENTS contents = ScanV3LibraryArchive( aFileName, aRequiredDocType );
659 return HasValidV3LibraryContents( contents, aRequiredDocType );
660 }
661 catch( ... )
662 {
663 return false;
664 }
665}
666
667
669{
670 m_rawDocs.clear();
671 m_project2 = nlohmann::json();
672 m_libraryIndex = nlohmann::json();
673 m_skippedCount = 0;
674
675 std::vector<V3_DOC_RAW> rawDocs = ParseV3DocsFromArchive( m_fileName, m_project2 );
676
677 for( V3_DOC_RAW& raw : rawDocs )
678 {
679 m_rawDocs[raw.docType][raw.uuid] = std::move( raw );
680 }
681}
682
683
685{
686 m_rawDocs.clear();
687 m_project2 = nlohmann::json();
688 m_libraryIndex = nlohmann::json();
689 m_skippedCount = 0;
690
691 std::vector<V3_DOC_RAW> rawDocs = ParseV3DocsFromLibraryArchive( m_fileName, m_libraryIndex );
692
693 for( V3_DOC_RAW& raw : rawDocs )
694 m_rawDocs[raw.docType][raw.uuid] = std::move( raw );
695}
696
697
698const std::map<wxString, V3_DOC_RAW>& V3_DOC_PARSER::GetRawDocs( const wxString& aDocType ) const
699{
700 static const std::map<wxString, V3_DOC_RAW> empty;
701
702 auto it = m_rawDocs.find( aDocType );
703
704 if( it == m_rawDocs.end() )
705 return empty;
706
707 return it->second;
708}
709
710
711const V3_DOC_RAW* V3_DOC_PARSER::FindRawDoc( const wxString& aDocType, const wxString& aUuid ) const
712{
713 auto typeIt = m_rawDocs.find( aDocType );
714
715 if( typeIt == m_rawDocs.end() )
716 return nullptr;
717
718 auto docIt = typeIt->second.find( aUuid );
719
720 if( docIt == typeIt->second.end() )
721 return nullptr;
722
723 return &docIt->second;
724}
725
726} // namespace EASYEDAPRO
int index
void Load()
Load all v3 docs from the archive.
const V3_DOC_RAW * FindRawDoc(const wxString &aDocType, const wxString &aUuid) const
const std::map< wxString, V3_DOC_RAW > & GetRawDocs(const wxString &aDocType) const
static bool IsV3Library(const wxString &aFileName, const wxString &aRequiredDocType=wxEmptyString)
Return true if aFileName looks like an EasyEDA Pro v3 library archive.
std::map< wxString, std::map< wxString, V3_DOC_RAW > > m_rawDocs
V3_DOC_PARSER(const wxString &aFileName)
static bool IsV3Archive(const wxString &aFileName)
Return true if aFileName looks like an EasyEDA Pro v3 archive.
void LoadLibrary()
Load all v3 docs from a library archive.
static bool empty(const wxTextEntryBase *aCtrl)
static std::string ToStdString(const wxString &aStr)
#define _(s)
#define THROW_IO_ERRORF(msg,...)
bool V3GetBool(const nlohmann::json &aObj, const char *aKey, bool aDefault)
nlohmann::json V3ParseIdArray(const wxString &aId)
bool V3IsNullOrMissing(const nlohmann::json &aObj, const char *aKey)
double V3GetDouble(const nlohmann::json &aObj, const char *aKey, double aDefault)
wxString V3JsonToString(const nlohmann::json &aValue, const wxString &aDefault)
int V3GetInt(const nlohmann::json &aObj, const char *aKey, int aDefault)
wxString V3GetString(const nlohmann::json &aObj, const char *aKey, const wxString &aDefault)
Raw parsed document from an EasyEDA Pro v3 .epru stream.
One parsed row from an EasyEDA Pro v3 .epru document stream.