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