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 {
310 THROW_IO_ERROR( wxString::Format( _( "Invalid DOCHEAD in '%s' at line %d" ),
311 aSource, currentLine ) );
312 }
313
314 currentDoc = std::move( doc );
315 currentLine++;
316 continue;
317 }
318
319 if( !currentDoc )
320 {
321 THROW_IO_ERROR( wxString::Format( _( "Expected DOCHEAD in '%s' at line %d" ),
322 aSource, currentLine ) );
323 }
324
325 V3_ROW row;
326 row.outer = std::move( outer );
327 row.inner = std::move( inner );
328 row.type = type;
329 row.id = GetString( row.outer, "id" );
330 row.ticket = GetInt( row.outer, "ticket", 0 );
331
332 wxString dedupId = row.id;
333
334 if( dedupId.empty() )
335 dedupId = wxString::Format( "__line_%d", currentLine );
336
337 auto it = currentDoc->rowById.find( dedupId );
338
339 if( it == currentDoc->rowById.end() )
340 {
341 currentDoc->rowById[dedupId] = currentDoc->rows.size();
342 currentDoc->rows.emplace_back( std::move( row ) );
343 }
344 else if( row.ticket >= currentDoc->rows[it->second].ticket )
345 {
346 currentDoc->rows[it->second] = std::move( row );
347 }
348 }
349 catch( nlohmann::json::exception& e )
350 {
351 THROW_IO_ERROR( wxString::Format( _( "JSON error in '%s' at line %d: %s" ),
352 aSource, currentLine, e.what() ) );
353 }
354
355 currentLine++;
356 }
357
358 flushDoc();
359}
360
361
362struct V3_LIBRARY_CONTENTS
363{
364 bool hasIndex = false;
365 bool hasElibu = false;
366 std::set<wxString> docTypes;
367};
368
369
370static bool HasValidV3LibraryContents( const V3_LIBRARY_CONTENTS& aContents, const wxString& aRequiredDocType )
371{
372 if( !aContents.hasIndex || !aContents.hasElibu )
373 return false;
374
375 if( aRequiredDocType.empty() )
376 return true;
377
378 return aContents.docTypes.contains( aRequiredDocType );
379}
380
381
382static void ScanEpruDocTypes( wxInputStream& aInput, const wxString& aSource, std::set<wxString>& aDocTypes )
383{
384 wxTextInputStream txt( aInput, wxS( " " ), wxConvUTF8 );
385 int currentLine = 1;
386
387 while( aInput.CanRead() )
388 {
389 wxString rawLine = txt.ReadLine();
390
391 if( rawLine.empty() )
392 {
393 currentLine++;
394 continue;
395 }
396
397 if( rawLine.Last() == '|' )
398 rawLine.RemoveLast();
399
400 int sepPos = rawLine.Find( wxS( "||" ) );
401
402 if( sepPos < 0 )
403 {
404 currentLine++;
405 continue;
406 }
407
408 wxString outerStr = rawLine.Left( sepPos );
409 wxString innerStr = rawLine.Mid( sepPos + 2 );
410
411 try
412 {
413 nlohmann::json outer = nlohmann::json::parse( ToStdString( outerStr ) );
414
415 if( GetString( outer, "type" ) == wxS( "DOCHEAD" ) && !innerStr.empty() )
416 {
417 nlohmann::json inner = nlohmann::json::parse( ToStdString( innerStr ) );
418 wxString docType = ParseDocType( inner );
419
420 if( !docType.empty() )
421 aDocTypes.insert( docType );
422 }
423 }
424 catch( nlohmann::json::exception& e )
425 {
427 wxString::Format( _( "JSON error in '%s' at line %d: %s" ), aSource, currentLine, e.what() ) );
428 }
429
430 currentLine++;
431 }
432}
433
434
435static nlohmann::json ReadJsonEntry( wxInputStream& aInput, const wxString& aEntryName )
436{
437 wxMemoryOutputStream mem;
438 mem << aInput;
439
440 wxStreamBuffer* buf = mem.GetOutputStreamBuffer();
441 std::string jsonStr( static_cast<const char*>( buf->GetBufferStart() ), buf->GetBufferSize() );
442
443 try
444 {
445 return nlohmann::json::parse( jsonStr );
446 }
447 catch( nlohmann::json::exception& e )
448 {
449 THROW_IO_ERROR( wxString::Format( _( "JSON error reading '%s': %s" ), aEntryName, e.what() ) );
450 }
451}
452
453
454static void MergeLibraryIndex( nlohmann::json& aTarget, nlohmann::json&& aIndex )
455{
456 if( !aIndex.is_object() )
457 return;
458
459 for( auto& [key, value] : aIndex.items() )
460 {
461 if( aTarget.contains( key ) && aTarget[key].is_object() && value.is_object() )
462 {
463 for( auto& [itemKey, itemValue] : value.items() )
464 aTarget[key][itemKey] = std::move( itemValue );
465 }
466 else if( !aTarget.contains( key ) || !value.is_object() || !value.empty() )
467 {
468 aTarget[key] = std::move( value );
469 }
470 }
471}
472
473
474static V3_LIBRARY_CONTENTS ScanV3LibraryArchive( const wxString& aFileName, const wxString& aRequiredDocType )
475{
476 V3_LIBRARY_CONTENTS contents;
477
478 std::shared_ptr<wxZipEntry> entry;
479 wxFFileInputStream in( aFileName );
480 wxZipInputStream zip( in );
481
482 if( !zip.IsOk() )
483 THROW_IO_ERROR( wxString::Format( _( "Cannot read ZIP archive '%s'" ), aFileName ) );
484
485 while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
486 {
487 wxString entryName = entry->GetName();
488 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' ).Lower();
489
490 if( baseName == wxS( "symbol2.json" ) || baseName == wxS( "footprint2.json" )
491 || baseName == wxS( "device2.json" ) )
492 {
493 contents.hasIndex = true;
494 }
495 else if( baseName.EndsWith( wxS( ".elibu" ) ) )
496 {
497 contents.hasElibu = true;
498
499 if( !aRequiredDocType.empty() )
500 ScanEpruDocTypes( zip, entryName, contents.docTypes );
501 }
502
503 if( HasValidV3LibraryContents( contents, aRequiredDocType ) )
504 break;
505 }
506
507 return contents;
508}
509
510
511static std::vector<V3_DOC_RAW> ParseV3DocsFromArchive( const wxString& aFileName, nlohmann::json& aProject2 )
512{
513 std::vector<V3_DOC_RAW> docs;
514
515 bool hasProject2 = false;
516 bool hasEpru = false;
517
518 std::shared_ptr<wxZipEntry> entry;
519 wxFFileInputStream in( aFileName );
520 wxZipInputStream zip( in );
521
522 if( !zip.IsOk() )
523 THROW_IO_ERROR( wxString::Format( _( "Cannot read ZIP archive '%s'" ), aFileName ) );
524
525 while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
526 {
527 wxString entryName = entry->GetName();
528 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
529
530 if( baseName.CmpNoCase( wxS( "project2.json" ) ) == 0 )
531 {
532 aProject2 = ReadJsonEntry( zip, entryName );
533 hasProject2 = true;
534 }
535 else if( baseName.Lower().EndsWith( wxS( ".epru" ) ) )
536 {
537 hasEpru = true;
538 ParseEpruStream( zip, entryName, docs );
539 }
540 }
541
542 if( !hasProject2 || !hasEpru )
543 {
544 THROW_IO_ERROR( wxString::Format( _( "'%s' does not appear to be a valid EasyEDA (JLCEDA) Pro v3 project. "
545 "Cannot find project2.json and .epru documents." ),
546 aFileName ) );
547 }
548
549 return docs;
550}
551
552
553static std::vector<V3_DOC_RAW> ParseV3DocsFromLibraryArchive( const wxString& aFileName, nlohmann::json& aLibraryIndex )
554{
555 std::vector<V3_DOC_RAW> docs;
556 V3_LIBRARY_CONTENTS contents;
557
558 std::shared_ptr<wxZipEntry> entry;
559 wxFFileInputStream in( aFileName );
560 wxZipInputStream zip( in );
561
562 if( !zip.IsOk() )
563 THROW_IO_ERROR( wxString::Format( _( "Cannot read ZIP archive '%s'" ), aFileName ) );
564
565 aLibraryIndex = nlohmann::json::object();
566
567 while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
568 {
569 wxString entryName = entry->GetName();
570 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
571 wxString lowerBaseName = baseName.Lower();
572
573 if( lowerBaseName == wxS( "symbol2.json" ) || lowerBaseName == wxS( "footprint2.json" )
574 || lowerBaseName == wxS( "device2.json" ) )
575 {
576 nlohmann::json index = ReadJsonEntry( zip, entryName );
577
578 MergeLibraryIndex( aLibraryIndex, std::move( index ) );
579
580 contents.hasIndex = true;
581 }
582 else if( lowerBaseName.EndsWith( wxS( ".elibu" ) ) )
583 {
584 contents.hasElibu = true;
585 ParseEpruStream( zip, entryName, docs );
586
587 for( const V3_DOC_RAW& doc : docs )
588 contents.docTypes.insert( doc.docType );
589 }
590 }
591
592 if( !HasValidV3LibraryContents( contents, wxEmptyString ) )
593 {
594 THROW_IO_ERROR( wxString::Format( _( "'%s' does not appear to be a valid EasyEDA (JLCEDA) Pro v3 library. "
595 "Cannot find symbol2.json, footprint2.json or device2.json and .elibu "
596 "documents." ),
597 aFileName ) );
598 }
599
600 return docs;
601}
602
603
604} // namespace
605
606
607namespace EASYEDAPRO
608{
609
610V3_DOC_PARSER::V3_DOC_PARSER( const wxString& aFileName ) :
611 m_fileName( aFileName )
612{
613}
614
615
616bool V3_DOC_PARSER::IsV3Archive( const wxString& aFileName )
617{
618 wxFileName fn( aFileName );
619 wxString ext = fn.GetExt().Lower();
620
621 if( ext != wxS( "epro2" ) && ext != wxS( "zip" ) )
622 return false;
623
624 try
625 {
626 std::shared_ptr<wxZipEntry> entry;
627 wxFFileInputStream in( aFileName );
628 wxZipInputStream zip( in );
629
630 if( !zip.IsOk() )
631 return false;
632
633 bool hasProject2 = false;
634 bool hasEpru = false;
635
636 while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL )
637 {
638 wxString entryName = entry->GetName();
639 wxString baseName = entryName.AfterLast( '\\' ).AfterLast( '/' );
640
641 if( baseName.CmpNoCase( wxS( "project2.json" ) ) == 0 )
642 hasProject2 = true;
643 else if( baseName.Lower().EndsWith( wxS( ".epru" ) ) )
644 hasEpru = true;
645
646 if( hasProject2 && hasEpru )
647 return true;
648 }
649
650 return false;
651 }
652 catch( ... )
653 {
654 return false;
655 }
656}
657
658
659bool V3_DOC_PARSER::IsV3Library( const wxString& aFileName, const wxString& aRequiredDocType )
660{
661 wxFileName fn( aFileName );
662
663 if( fn.GetExt().Lower() != wxS( "elibz2" ) )
664 return false;
665
666 try
667 {
668 V3_LIBRARY_CONTENTS contents = ScanV3LibraryArchive( aFileName, aRequiredDocType );
669 return HasValidV3LibraryContents( contents, aRequiredDocType );
670 }
671 catch( ... )
672 {
673 return false;
674 }
675}
676
677
679{
680 m_rawDocs.clear();
681 m_project2 = nlohmann::json();
682 m_libraryIndex = nlohmann::json();
683 m_skippedCount = 0;
684
685 std::vector<V3_DOC_RAW> rawDocs = ParseV3DocsFromArchive( m_fileName, m_project2 );
686
687 for( V3_DOC_RAW& raw : rawDocs )
688 {
689 m_rawDocs[raw.docType][raw.uuid] = std::move( raw );
690 }
691}
692
693
695{
696 m_rawDocs.clear();
697 m_project2 = nlohmann::json();
698 m_libraryIndex = nlohmann::json();
699 m_skippedCount = 0;
700
701 std::vector<V3_DOC_RAW> rawDocs = ParseV3DocsFromLibraryArchive( m_fileName, m_libraryIndex );
702
703 for( V3_DOC_RAW& raw : rawDocs )
704 m_rawDocs[raw.docType][raw.uuid] = std::move( raw );
705}
706
707
708const std::map<wxString, V3_DOC_RAW>& V3_DOC_PARSER::GetRawDocs( const wxString& aDocType ) const
709{
710 static const std::map<wxString, V3_DOC_RAW> empty;
711
712 auto it = m_rawDocs.find( aDocType );
713
714 if( it == m_rawDocs.end() )
715 return empty;
716
717 return it->second;
718}
719
720
721const V3_DOC_RAW* V3_DOC_PARSER::FindRawDoc( const wxString& aDocType, const wxString& aUuid ) const
722{
723 auto typeIt = m_rawDocs.find( aDocType );
724
725 if( typeIt == m_rawDocs.end() )
726 return nullptr;
727
728 auto docIt = typeIt->second.find( aUuid );
729
730 if( docIt == typeIt->second.end() )
731 return nullptr;
732
733 return &docIt->second;
734}
735
736} // 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_ERROR(msg)
macro which captures the "call site" values of FILE_, __FUNCTION & LINE
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.