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