KiCad PCB EDA Suite
Loading...
Searching...
No Matches
panel_remote_symbol.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * 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
24#include <wx/wupdlock.h>
25
26#include "picosha2.h"
27
28#include "panel_remote_symbol.h"
29
30#include <bitmaps.h>
31#include <build_version.h>
32#include <common.h>
33#include <ki_exception.h>
34#include <pgm_base.h>
35#include <project.h>
36#include <project_sch.h>
37#include <remote_login_server.h>
38#include <sch_edit_frame.h>
39#include <sch_symbol.h>
41#include <eeschema_settings.h>
46#include <lib_symbol.h>
47#include <richio.h>
49#include <sch_io/sch_io_mgr.h>
50#include <sch_io/sch_io.h>
51#include <tool/common_tools.h>
52#include <tool/tool_manager.h>
53#include <tools/sch_actions.h>
56#include <nlohmann/json.hpp>
57#include <algorithm>
58#include <cctype>
59#include <fstream>
60#include <map>
61#include <set>
62
63#ifndef wxUSE_BASE64
64#define wxUSE_BASE64 1
65#endif
66#include <wx/base64.h>
67
68#include <wx/datetime.h>
69#include <wx/choice.h>
70#include <wx/dir.h>
71#include <wx/ffile.h>
72#include <wx/filefn.h>
73#include <wx/intl.h>
74#include <wx/log.h>
75#include <wx/mstream.h>
76#include <wx/sizer.h>
77#include <wx/strconv.h>
78#include <wx/webview.h>
79#include <wx/socket.h>
80#include <wx/timer.h>
81#include <wx/tokenzr.h>
82#include <wx/uri.h>
83#include <wx/utils.h>
84#include <zstd.h>
85
86
87namespace
88{
89
90class STRING_OUTPUTFORMATTER_BUFFER : public OUTPUTFORMATTER
91{
92public:
93 STRING_OUTPUTFORMATTER_BUFFER() : OUTPUTFORMATTER( OUTPUTFMTBUFZ ) {}
94
95 const std::string& GetString() const { return m_output; }
96
97protected:
98 void write( const char* aOutBuf, int aCount ) override
99 {
100 m_output.append( aOutBuf, aCount );
101 }
102
103private:
104 std::string m_output;
105};
106
107std::string HashBuffer( const std::vector<uint8_t>& aBuffer )
108{
109 std::vector<unsigned char> hash( picosha2::k_digest_size );
110 picosha2::hash256( aBuffer.begin(), aBuffer.end(), hash.begin(), hash.end() );
111 return picosha2::bytes_to_hex_string( hash.begin(), hash.end() );
112}
113
114std::string HashString( const std::string& aValue )
115{
116 std::vector<unsigned char> hash( picosha2::k_digest_size );
117 picosha2::hash256( aValue.begin(), aValue.end(), hash.begin(), hash.end() );
118 return picosha2::bytes_to_hex_string( hash.begin(), hash.end() );
119}
120
121bool HashFile( const wxFileName& aPath, std::string& aOutHash )
122{
123 if( !aPath.FileExists() )
124 return false;
125
126 std::ifstream file( aPath.GetFullPath().ToStdString(), std::ios::binary );
127
128 if( !file )
129 return false;
130
131 std::vector<unsigned char> hash( picosha2::k_digest_size );
132 picosha2::hash256( file, hash.begin(), hash.end() );
133 aOutHash = picosha2::bytes_to_hex_string( hash.begin(), hash.end() );
134 return true;
135}
136
137wxString AppendNumericSuffix( const wxString& aBase, int aSuffix )
138{
139 return wxString::Format( wxS( "%s_%d" ), aBase, aSuffix );
140}
141
142wxString AppendNumericSuffixToFilename( const wxString& aFilename, int aSuffix )
143{
144 wxFileName fn;
145 fn.SetFullName( aFilename );
146
147 const wxString base = fn.GetName();
148 const wxString ext = fn.GetExt();
149 wxString candidate = AppendNumericSuffix( base, aSuffix );
150
151 if( ext.IsEmpty() )
152 return candidate;
153
154 return candidate + wxS( "." ) + ext;
155}
156
157std::string SerializeSymbolCanonical( const LIB_SYMBOL& aSymbol )
158{
159 LIB_SYMBOL clone( aSymbol );
160 STRING_OUTPUTFORMATTER_BUFFER formatter;
161 SCH_IO_KICAD_SEXPR_LIB_CACHE::SaveSymbol( &clone, formatter );
162 return formatter.GetString();
163}
164
165std::unique_ptr<LIB_SYMBOL> TryCloneSymbol( SCH_IO* aPlugin, const wxFileName& aLibraryFile,
166 const wxString& aSymbolName )
167{
168 if( !aPlugin || !aLibraryFile.FileExists() )
169 return nullptr;
170
171 try
172 {
173 LIB_SYMBOL* existing = aPlugin->LoadSymbol( aLibraryFile.GetFullPath(), aSymbolName );
174
175 if( !existing )
176 return nullptr;
177
178 return std::make_unique<LIB_SYMBOL>( *existing );
179 }
180 catch( const IO_ERROR& )
181 {
182 return nullptr;
183 }
184}
185
186bool ComputeSymbolChecksum( SCH_IO* aPlugin, const wxFileName& aLibraryFile,
187 const wxString& aSymbolName, std::string& aOutChecksum )
188{
189 std::unique_ptr<LIB_SYMBOL> symbol = TryCloneSymbol( aPlugin, aLibraryFile, aSymbolName );
190
191 if( !symbol )
192 return false;
193
194 aOutChecksum = HashString( SerializeSymbolCanonical( *symbol ) );
195 return true;
196}
197
198} // namespace
199
200
201
202wxString PANEL_REMOTE_SYMBOL::jsonString( const nlohmann::json& aObject, const char* aKey ) const
203{
204 auto it = aObject.find( aKey );
205
206 if( it != aObject.end() && it->is_string() )
207 return wxString::FromUTF8( it->get<std::string>() );
208
209 return wxString();
210}
211
212bool PANEL_REMOTE_SYMBOL::decodeBase64Payload( const std::string& aEncoded,
213 std::vector<uint8_t>& aOutput,
214 wxString& aError ) const
215{
216 if( aEncoded.empty() )
217 {
218 aError = _( "Missing payload data." );
219 return false;
220 }
221
222 wxMemoryBuffer buffer = wxBase64Decode( wxString::FromUTF8( aEncoded.c_str() ) );
223
224 if( buffer.IsEmpty() )
225 {
226 aError = _( "Failed to decode base64 payload." );
227 return false;
228 }
229
230 aOutput.resize( buffer.GetDataLen() );
231 memcpy( aOutput.data(), buffer.GetData(), buffer.GetDataLen() );
232 return true;
233}
234
235bool PANEL_REMOTE_SYMBOL::decompressIfNeeded( const std::string& aCompression,
236 const std::vector<uint8_t>& aInput,
237 std::vector<uint8_t>& aOutput,
238 wxString& aError ) const
239{
240 if( aCompression.empty() || aCompression == "NONE" )
241 {
242 aOutput = aInput;
243 return true;
244 }
245
246 if( aCompression != "ZSTD" )
247 {
248 aError = wxString::Format( _( "Unsupported compression '%s'." ), wxString::FromUTF8( aCompression ) );
249 return false;
250 }
251
252 if( aInput.empty() )
253 {
254 aError = _( "Compressed payload was empty." );
255 return false;
256 }
257
258 unsigned long long expectedSize = ZSTD_getFrameContentSize( aInput.data(), aInput.size() );
259
260 if( expectedSize == ZSTD_CONTENTSIZE_ERROR || expectedSize == ZSTD_CONTENTSIZE_UNKNOWN )
261 expectedSize = static_cast<unsigned long long>( aInput.size() ) * 4;
262
263 aOutput.resize( expectedSize );
264
265 size_t decompressed = ZSTD_decompress( aOutput.data(), expectedSize, aInput.data(), aInput.size() );
266
267 if( ZSTD_isError( decompressed ) )
268 {
269 aError = wxString::Format( _( "ZSTD decompression failed: %s" ),
270 wxString::FromUTF8( ZSTD_getErrorName( decompressed ) ) );
271 return false;
272 }
273
274 aOutput.resize( decompressed );
275 return true;
276}
277
278wxString PANEL_REMOTE_SYMBOL::sanitizeForScript( const std::string& aJson ) const
279{
280 wxString script = wxString::FromUTF8( aJson.c_str() );
281 script.Replace( "\\", "\\\\" );
282 script.Replace( "'", "\\'" );
283 return script;
284}
285
286wxString PANEL_REMOTE_SYMBOL::normalizeDataSourceUrl( const wxString& aUrl ) const
287{
288 wxString normalized = aUrl;
289 normalized.Trim( true ).Trim( false );
290
291 while( normalized.Length() > 1 && normalized.EndsWith( wxS( "/" ) ) )
292 normalized.RemoveLast();
293
294 return normalized;
295}
296
301
303{
304 m_activeUserId.clear();
305
306 if( m_activeDataSourceUrl.IsEmpty() )
307 return;
308
309 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
310 {
311 auto it = settings->m_RemoteSymbol.user_ids.find( m_activeDataSourceUrl );
312
313 if( it != settings->m_RemoteSymbol.user_ids.end() )
314 m_activeUserId = it->second;
315 }
316}
317
319{
320 if( aUserId.IsEmpty() )
321 return;
322
323 if( m_activeDataSourceUrl.IsEmpty() )
324 return;
325
326 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
327 {
328 settings->m_RemoteSymbol.user_ids[m_activeDataSourceUrl] = aUserId;
329 Pgm().GetSettingsManager().Save( settings );
330 }
331
332 m_activeUserId = aUserId;
333}
334
335wxString PANEL_REMOTE_SYMBOL::sanitizeFileComponent( const wxString& aValue,
336 const wxString& aDefault ) const
337{
338 wxString result = aValue;
339 result.Trim( true ).Trim( false );
340
341 if( result.IsEmpty() )
342 result = aDefault;
343
344 for( size_t i = 0; i < result.length(); ++i )
345 {
346 wxUniChar ch = result[i];
347
348 if( ch == '/' || ch == '\\' || ch == ':' )
349 result[i] = '_';
350 }
351
352 return result;
353}
354
355bool PANEL_REMOTE_SYMBOL::writeBinaryFile( const wxFileName& aOutput,
356 const std::vector<uint8_t>& aPayload,
357 wxString& aError ) const
358{
359 if( aPayload.empty() )
360 {
361 aError = _( "Payload was empty." );
362 return false;
363 }
364
365 wxFileName targetDir = aOutput;
366 targetDir.SetFullName( wxEmptyString );
367
368 if( !targetDir.DirExists() )
369 {
370 if( !targetDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
371 {
372 aError = wxString::Format( _( "Unable to create '%s'." ), targetDir.GetFullPath() );
373 return false;
374 }
375 }
376
377 wxFFile file( aOutput.GetFullPath(), wxS( "wb" ) );
378
379 if( !file.IsOpened() )
380 {
381 aError = wxString::Format( _( "Unable to open '%s' for writing." ), aOutput.GetFullPath() );
382 return false;
383 }
384
385 if( file.Write( aPayload.data(), aPayload.size() ) != aPayload.size() )
386 {
387 aError = wxString::Format( _( "Failed to write '%s'." ), aOutput.GetFullPath() );
388 return false;
389 }
390
391 file.Close();
392 return true;
393}
394
395
396std::unique_ptr<LIB_SYMBOL> PANEL_REMOTE_SYMBOL::loadSymbolFromPayload( const std::vector<uint8_t>& aPayload,
397 const wxString& aLibItemName,
398 wxString& aError ) const
399{
400 if( aPayload.empty() )
401 {
402 aError = _( "Symbol payload was empty." );
403 return nullptr;
404 }
405
406 wxString tempPath = wxFileName::CreateTempFileName( wxS( "remote_symbol" ) );
407
408 if( tempPath.IsEmpty() )
409 {
410 aError = _( "Unable to create a temporary file for the symbol payload." );
411 return nullptr;
412 }
413
414 wxFileName tempFile( tempPath );
415
416 wxFFile file( tempFile.GetFullPath(), wxS( "wb" ) );
417
418 if( !file.IsOpened() )
419 {
420 aError = _( "Unable to create a temporary file for the symbol payload." );
421 wxRemoveFile( tempFile.GetFullPath() );
422 return nullptr;
423 }
424
425 if( file.Write( aPayload.data(), aPayload.size() ) != aPayload.size() )
426 {
427 aError = _( "Failed to write the temporary symbol payload." );
428 file.Close();
429 wxRemoveFile( tempFile.GetFullPath() );
430 return nullptr;
431 }
432
433 file.Close();
434
435 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
436
437 if( !plugin )
438 {
439 aError = _( "Unable to access the KiCad symbol plugin." );
440 wxRemoveFile( tempFile.GetFullPath() );
441 return nullptr;
442 }
443
444 std::unique_ptr<LIB_SYMBOL> symbol;
445
446 try
447 {
448 LIB_SYMBOL* loaded = plugin->LoadSymbol( tempFile.GetFullPath(), aLibItemName );
449
450 if( loaded )
451 {
452 // Clone the symbol before the plugin's cache is destroyed.
453 // LoadSymbol returns a pointer owned by the plugin's internal cache,
454 // and the cache will be destroyed when 'plugin' goes out of scope.
455 symbol = std::make_unique<LIB_SYMBOL>( *loaded );
456 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
457 "loadSymbolFromPayload: loaded symbol %s from temporary file %s",
458 aLibItemName.ToUTF8().data(), tempFile.GetFullPath().ToUTF8().data() );
459 }
460 else
461 {
462 aError = _( "Symbol payload did not include the expected symbol." );
463 }
464 }
465 catch( const IO_ERROR& e )
466 {
467 aError = wxString::Format( _( "Unable to decode the symbol payload: %s" ), e.What() );
468 }
469
470 wxRemoveFile( tempFile.GetFullPath() );
471 return symbol;
472}
473
474
476 wxPanel( aParent ),
477 m_frame( aParent ),
478 m_dataSourceChoice( nullptr ),
479 m_configButton( nullptr ),
480 m_refreshButton( nullptr ),
481 m_webView( nullptr ),
482 m_pcm( std::make_shared<PLUGIN_CONTENT_MANAGER>( []( int ) {} ) ),
483 m_sessionId( 0 ),
484 m_messageIdCounter( 0 ),
485 m_pendingHandshake( false ),
486 m_loginServer(),
487 m_activeDataSourceUrl(),
488 m_activeUserId(),
489 m_webViewLoadedBound( false )
490{
491 wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL );
492
493 wxBoxSizer* controlsSizer = new wxBoxSizer( wxHORIZONTAL );
494 m_dataSourceChoice = new wxChoice( this, wxID_ANY );
495 m_dataSourceChoice->SetMinSize( FromDIP( wxSize( 160, -1 ) ) );
496 m_dataSourceChoice->SetToolTip( _( "Select which remote data source to query." ) );
497 controlsSizer->Add( m_dataSourceChoice, 1, wxEXPAND | wxRIGHT, FromDIP( 2 ) );
498
499 m_refreshButton = new BITMAP_BUTTON( this, wxID_ANY );
500 m_refreshButton->SetBitmap( KiBitmapBundle( BITMAPS::reload ) );
501 m_refreshButton->SetPadding( FromDIP( 2 ) );
502 m_refreshButton->SetToolTip( _( "Refresh" ) );
503 controlsSizer->Add( m_refreshButton, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP( 2 ) );
504
505 m_configButton = new BITMAP_BUTTON( this, wxID_ANY );
506 m_configButton->SetBitmap( KiBitmapBundle( BITMAPS::config ) );
507 m_configButton->SetPadding( FromDIP( 2 ) );
508 m_configButton->SetToolTip( _( "Configure remote data sources." ) );
509 controlsSizer->Add( m_configButton, 0, wxALIGN_CENTER_VERTICAL );
510
511 topSizer->Add( controlsSizer, 0, wxEXPAND | wxALL, FromDIP( 4 ) );
512
513 m_webView = new WEBVIEW_PANEL( this );
514 m_webView->AddMessageHandler( wxS( "kicad" ),
515 [this]( const wxString& payload )
516 {
517 onKicadMessage( payload );
518 } );
519 m_webView->SetHandleExternalLinks( true );
520
521 topSizer->Add( m_webView, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP( 2 ) );
522
523 SetSizer( topSizer );
524
525 m_dataSourceChoice->Bind( wxEVT_CHOICE, &PANEL_REMOTE_SYMBOL::onDataSourceChanged, this );
526 m_configButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onConfigure, this );
527 m_refreshButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onRefresh, this );
528 Bind( EVT_REMOTE_SYMBOL_LOGIN_RESULT, &PANEL_REMOTE_SYMBOL::onRemoteLoginResult, this );
529
530 RefreshDataSources();
531
532 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "PANEL_REMOTE_SYMBOL constructed (frame=%p)", (void*)aParent );
533}
534
535
537{
539 return;
540
541 if( wxWebView* browser = m_webView->GetWebView() )
542 {
543 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
544 m_webView->BindLoadedEvent();
546 }
547}
548
549
551{
552 if( KICAD_SETTINGS* cfg = GetAppSettings<KICAD_SETTINGS>( "kicad" ) )
553 m_pcm->SetRepositoryList( cfg->m_PcmRepositories );
554
555 m_dataSources.clear();
556 m_dataSourceChoice->Clear();
557
558 const std::vector<PCM_INSTALLATION_ENTRY> installed = m_pcm->GetInstalledPackages();
559
560 for( const PCM_INSTALLATION_ENTRY& entry : installed )
561 {
562 if( entry.package.type != PT_DATASOURCE )
563 continue;
564
565 wxString label = entry.package.name;
566
567 if( !entry.current_version.IsEmpty() )
568 label << wxS( " (" ) << entry.current_version << wxS( ")" );
569
570 m_dataSources.push_back( entry );
571 m_dataSourceChoice->Append( label );
572 }
573
574 if( m_dataSources.empty() )
575 {
576 m_dataSourceChoice->Enable( false );
577 showMessage( _( "No remote data sources are currently installed." ) );
578 return;
579 }
580
581 m_dataSourceChoice->Enable( true );
582 m_dataSourceChoice->SetSelection( 0 );
583 loadDataSource( 0 );
584}
585
586
587void PANEL_REMOTE_SYMBOL::onDataSourceChanged( wxCommandEvent& aEvent )
588{
589 const int selection = aEvent.GetSelection();
590
591 if( selection == wxNOT_FOUND )
592 return;
593
594 loadDataSource( static_cast<size_t>( selection ) );
595}
596
597
598void PANEL_REMOTE_SYMBOL::onConfigure( wxCommandEvent& aEvent )
599{
600 DIALOG_REMOTE_SYMBOL_CONFIG dlg( this );
601
602 dlg.ShowModal();
603
605}
606
607
608void PANEL_REMOTE_SYMBOL::onRefresh( wxCommandEvent& aEvent )
609{
610 if( m_webView && m_webView->GetWebView() )
611 m_webView->GetWebView()->Reload();
612}
613
614
616{
617 if( aIndex >= m_dataSources.size() )
618 return false;
619
620 return loadDataSource( m_dataSources[aIndex] );
621}
622
623
625{
626 return !m_dataSources.empty();
627}
628
629
631{
633
634 std::optional<wxFileName> jsonPath = findDataSourceJson( aEntry );
635
636 if( !jsonPath )
637 {
638 wxLogWarning( "No JSON configuration found for data source %s", aEntry.package.identifier );
639 showMessage( wxString::Format( _( "No configuration JSON found for '%s'." ),
640 aEntry.package.name ) );
641 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: no json for %s", aEntry.package.identifier );
642 return false;
643 }
644
645 wxFFile file( jsonPath->GetFullPath(), "rb" );
646
647 if( !file.IsOpened() )
648 {
649 wxLogWarning( "Unable to open remote data source JSON: %s", jsonPath->GetFullPath() );
650 showMessage( wxString::Format( _( "Unable to open '%s'." ), jsonPath->GetFullPath() ) );
651 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: cannot open %s", jsonPath->GetFullPath() );
652 return false;
653 }
654
655 wxString jsonContent;
656
657 if( !file.ReadAll( &jsonContent, wxConvUTF8 ) )
658 {
659 wxLogWarning( "Failed to read remote data source JSON: %s", jsonPath->GetFullPath() );
660 showMessage( wxString::Format( _( "Unable to read '%s'." ), jsonPath->GetFullPath() ) );
661 return false;
662 }
663
664 if( std::optional<wxString> url = extractUrlFromJson( jsonContent ) )
665 {
668 m_pendingHandshake = true;
669 m_webView->LoadURL( *url );
670 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: loading URL %s", (*url).ToUTF8().data() );
671 return true;
672 }
673
674 wxLogWarning( "Remote data source JSON did not produce a valid URL for %s", aEntry.package.identifier );
675 showMessage( wxString::Format( _( "Unable to load remote data for '%s'." ), aEntry.package.name ) );
676 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: failed to find URL in %s", jsonPath->GetFullPath() );
677 return false;
678}
679
680
681std::optional<wxFileName>
683{
684 wxString cleanId = aEntry.package.identifier;
685 cleanId.Replace( '.', '_' );
686
687 wxFileName baseDir = wxFileName::DirName( m_pcm->Get3rdPartyPath() );
688 baseDir.AppendDir( wxS( "resources" ) );
689 baseDir.AppendDir( cleanId );
690
691 const wxString resourcesPath = baseDir.GetFullPath();
692
693 if( !wxDirExists( resourcesPath ) )
694 return std::nullopt;
695
696 const std::vector<wxString> preferredNames = {
697 wxS( "remote_symbol.json" ),
698 wxS( "datasource.json" )
699 };
700
701 for( const wxString& candidate : preferredNames )
702 {
703 wxFileName file( baseDir );
704 file.SetFullName( candidate );
705
706 if( file.FileExists() )
707 return file;
708 }
709
710 wxDir dir( resourcesPath );
711
712 if( !dir.IsOpened() )
713 return std::nullopt;
714
715 wxString jsonFile;
716
717 if( dir.GetFirst( &jsonFile, wxS( "*.json" ), wxDIR_FILES ) )
718 {
719 wxFileName fallback( baseDir );
720 fallback.SetFullName( jsonFile );
721 return fallback;
722 }
723
724 return std::nullopt;
725}
726
727
728void PANEL_REMOTE_SYMBOL::showMessage( const wxString& aMessage )
729{
730 if( !m_webView )
731 return;
732
733 wxString html;
734 wxString escaped = aMessage;
735 escaped.Replace( "&", "&amp;" );
736 escaped.Replace( "<", "&lt;" );
737 escaped.Replace( ">", "&gt;" );
738 html << wxS( "<html><body><p>" ) << escaped << wxS( "</p></body></html>" );
739 m_webView->SetPage( html );
740}
741
742
743std::optional<wxString> PANEL_REMOTE_SYMBOL::extractUrlFromJson( const wxString& aJsonContent ) const
744{
745 if( aJsonContent.IsEmpty() )
746 return std::nullopt;
747
748 wxScopedCharBuffer utf8 = aJsonContent.ToUTF8();
749
750 if( !utf8 || utf8.length() == 0 )
751 return std::nullopt;
752
753 try
754 {
755 nlohmann::json parsed = nlohmann::json::parse( utf8.data() );
756 std::string url;
757
758 if( parsed.is_string() )
759 {
760 url = parsed.get<std::string>();
761 }
762 else if( parsed.is_object() )
763 {
764 auto extractString = [&]( const char* key ) -> std::string
765 {
766 auto it = parsed.find( key );
767
768 if( it != parsed.end() && it->is_string() )
769 return it->get<std::string>();
770
771 return {};
772 };
773
774 auto extractInt = [&]( const char* key ) -> std::optional<int>
775 {
776 auto it = parsed.find( key );
777
778 if( it == parsed.end() )
779 return std::nullopt;
780
781 if( it->is_number_integer() )
782 return it->get<int>();
783
784 if( it->is_string() )
785 {
786 try
787 {
788 return std::stoi( it->get<std::string>() );
789 }
790 catch( ... )
791 {
792 }
793 }
794
795 return std::nullopt;
796 };
797
798 std::string host = extractString( "host" );
799 std::optional<int> port = extractInt( "port" );
800 std::string path = extractString( "path" );
801
802 if( !host.empty() )
803 {
804 if( path.empty() )
805 path = "/";
806
807 if( port && *port > 0 )
808 url = wxString::Format( "%s:%d%s", host, *port, path ).ToStdString();
809 else
810 url = host + path;
811 }
812
813 if( url.empty() )
814 url = extractString( "url" );
815
816 if( url.empty() )
817 {
818 for( const char* key : { "website", "endpoint" } )
819 {
820 url = extractString( key );
821
822 if( !url.empty() )
823 break;
824 }
825 }
826
827 if( url.empty() )
828 {
829 for( const auto& [name, value] : parsed.items() )
830 {
831 if( value.is_string() )
832 {
833 const std::string candidate = value.get<std::string>();
834
835 if( candidate.rfind( "http", 0 ) == 0 || candidate.rfind( "file", 0 ) == 0 )
836 {
837 url = candidate;
838 break;
839 }
840 }
841 }
842 }
843 }
844
845 if( url.empty() )
846 return std::nullopt;
847
848 return wxString::FromUTF8( url.c_str() );
849 }
850 catch( const std::exception& e )
851 {
852 wxLogWarning( "Failed to parse remote symbol JSON: %s", e.what() );
853 return std::nullopt;
854 }
855}
856
857
858void PANEL_REMOTE_SYMBOL::onKicadMessage( const wxString& aMessage )
859{
860 wxScopedCharBuffer utf8 = aMessage.ToUTF8();
861
862 if( !utf8 || utf8.length() == 0 )
863 {
864 wxLogWarning( "Remote symbol RPC: empty payload." );
865 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: empty payload" );
866 return;
867 }
868
869 try
870 {
871 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: received payload size=%d", (int)utf8.length() );
872 handleRpcMessage( nlohmann::json::parse( utf8.data() ) );
873 }
874 catch( const std::exception& e )
875 {
876 wxLogWarning( "Remote symbol RPC parse error: %s", e.what() );
877 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: parse error %s", e.what() );
878 }
879}
880
881
882void PANEL_REMOTE_SYMBOL::onWebViewLoaded( wxWebViewEvent& aEvent )
883{
884 wxUnusedVar( aEvent );
885
887 {
888 CallAfter( [this]()
889 {
891 {
892 m_pendingHandshake = false;
894 }
895 } );
896 }
897
898 aEvent.Skip();
899}
900
901
903{
904 if( !m_webView )
905 return;
906
907 m_sessionId = KIID();
909
910 nlohmann::json params = nlohmann::json::object();
911 params["client_name"] = "KiCad";
912 params["client_version"] = GetSemanticVersion().ToStdString();
913 params["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
914
915 sendRpcMessage( wxS( "NEW_SESSION" ), std::move( params ) );
916 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "beginSessionHandshake: NEW_SESSION sent, session=%s", m_sessionId.AsString() );
917}
918
924
925void PANEL_REMOTE_SYMBOL::handleRemoteLogin( const nlohmann::json& aParams, int aMessageId )
926{
927 const wxString loginUrl = jsonString( aParams, "url" );
928
929 if( loginUrl.IsEmpty() )
930 {
931 respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INVALID_PARAMETERS" ),
932 _( "Missing login URL for remote authentication." ) );
933 return;
934 }
935
937
938 std::unique_ptr<REMOTE_LOGIN_SERVER> server = std::make_unique<REMOTE_LOGIN_SERVER>( this, "https://www.google.com/" );
939
940 if( !server->Start() )
941 {
942 respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INTERNAL_ERROR" ),
943 _( "Unable to start the local login listener." ) );
944 return;
945 }
946
947 const unsigned short port = server->GetPort();
948
949 if( port == 0 )
950 {
951 respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INTERNAL_ERROR" ),
952 _( "Failed to allocate a callback port for login." ) );
953 return;
954 }
955
956 m_loginServer = std::move( server );
957
958 nlohmann::json reply = nlohmann::json::object();
959 reply["port"] = static_cast<int>( port );
960 sendRpcMessage( wxS( "REMOTE_LOGIN" ), std::move( reply ), aMessageId );
961
962 wxString launchUrl = loginUrl;
963
964 if( launchUrl.Find( '?' ) != wxNOT_FOUND )
965 launchUrl << "&port=" << port;
966 else
967 launchUrl << "?port=" << port;
968
969 if( !wxLaunchDefaultBrowser( launchUrl ) )
970 {
971 wxLogWarning( "Remote login requested but default browser could not be opened (%s).",
972 launchUrl.ToUTF8().data() );
973 }
974}
975
976void PANEL_REMOTE_SYMBOL::onRemoteLoginResult( wxCommandEvent& aEvent )
977{
978 const bool success = aEvent.GetInt() != 0;
979 const wxString userId = aEvent.GetString();
980
981 if( success && !userId.IsEmpty() )
982 {
984 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "Remote login succeeded; stored user_id." );
985
986 if( m_webView && !m_activeDataSourceUrl.IsEmpty() )
988 }
989 else
990 {
991 wxLogWarning( "Remote login callback did not provide a user_id." );
992 }
993
995}
996
997
998void PANEL_REMOTE_SYMBOL::sendRpcMessage( const wxString& aCommand,
999 nlohmann::json aParameters,
1000 std::optional<int> aResponseTo,
1001 const wxString& aStatus,
1002 const std::string& aData,
1003 const wxString& aErrorCode,
1004 const wxString& aErrorMessage )
1005{
1006 if( !m_webView || m_webView->HasLoadError() )
1007 return;
1008
1009 nlohmann::json payload = nlohmann::json::object();
1010 payload["version"] = REMOTE_SYMBOL_SESSION_VERSION;
1011 payload["session_id"] = m_sessionId.AsStdString();
1012 payload["message_id"] = ++m_messageIdCounter;
1013 payload["command"] = aCommand.ToStdString();
1014
1015 if( !m_activeUserId.IsEmpty() )
1016 payload["user_id"] = m_activeUserId.ToStdString();
1017
1018 if( aResponseTo )
1019 payload["response_to"] = *aResponseTo;
1020
1021 if( !aStatus.IsEmpty() )
1022 payload["status"] = aStatus.ToStdString();
1023
1024 if( !aParameters.is_null() && !aParameters.empty() )
1025 payload["parameters"] = std::move( aParameters );
1026
1027 if( !aData.empty() )
1028 payload["data"] = aData;
1029
1030 if( !aErrorCode.IsEmpty() )
1031 payload["error_code"] = aErrorCode.ToStdString();
1032
1033 if( !aErrorMessage.IsEmpty() )
1034 payload["error_message"] = aErrorMessage.ToStdString();
1035
1036 wxString script = wxString::Format( wxS( "window.kiclient.postMessage('%s');" ),
1037 sanitizeForScript( payload.dump() ) );
1038 m_webView->RunScriptAsync( script );
1039 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "sendRpcMessage: %s", script );
1040}
1041
1042
1043void PANEL_REMOTE_SYMBOL::respondWithError( const wxString& aCommand, int aResponseTo,
1044 const wxString& aErrorCode,
1045 const wxString& aErrorMessage )
1046{
1047 sendRpcMessage( aCommand, nlohmann::json::object(), aResponseTo, wxS( "ERROR" ),
1048 std::string(), aErrorCode, aErrorMessage );
1049}
1050
1051
1052void PANEL_REMOTE_SYMBOL::handleRpcMessage( const nlohmann::json& aMessage )
1053{
1054 if( !aMessage.is_object() )
1055 return;
1056
1057 const wxString command = jsonString( aMessage, "command" );
1058
1059 if( command.IsEmpty() )
1060 return;
1061
1062 auto messageIdIt = aMessage.find( "message_id" );
1063
1064 if( messageIdIt == aMessage.end() || !messageIdIt->is_number_integer() )
1065 return;
1066
1067 const int messageId = messageIdIt->get<int>();
1068
1069 const int version = aMessage.value( "version", 0 );
1070
1071 if( version != REMOTE_SYMBOL_SESSION_VERSION )
1072 {
1073 respondWithError( command, messageId, wxS( "UNSUPPORTED_VERSION" ),
1074 wxString::Format( _( "Unsupported RPC version %d." ), version ) );
1075 return;
1076 }
1077
1078 const wxString sessionId = jsonString( aMessage, "session_id" );
1079
1080 if( sessionId.IsEmpty() )
1081 {
1082 respondWithError( command, messageId, wxS( "INVALID_PARAMETERS" ),
1083 _( "Missing session identifier." ) );
1084 return;
1085 }
1086
1087 if( !sessionId.IsSameAs( m_sessionId.AsString() ) )
1088 wxLogWarning( "Remote symbol RPC session mismatch (expected %s, got %s).",
1089 m_sessionId.AsString(), sessionId );
1090
1091 const wxString status = jsonString( aMessage, "status" );
1092
1093 if( status.IsSameAs( wxS( "ERROR" ), false ) )
1094 {
1095 wxLogWarning( "Remote symbol RPC error (%s): %s", jsonString( aMessage, "error_code" ),
1096 jsonString( aMessage, "error_message" ) );
1097 return;
1098 }
1099
1100 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "handleRpcMessage: command=%s message_id=%d status=%s session=%s",
1101 command.ToUTF8().data(), messageId, status.ToUTF8().data(), sessionId.ToUTF8().data() );
1102
1103 nlohmann::json params = nlohmann::json::object();
1104 auto paramsIt = aMessage.find( "parameters" );
1105
1106 if( paramsIt != aMessage.end() && paramsIt->is_object() )
1107 params = *paramsIt;
1108
1109 const std::string data = aMessage.value( "data", std::string() );
1110
1111 if( command == wxS( "NEW_SESSION" ) )
1112 {
1113 nlohmann::json reply = nlohmann::json::object();
1114 reply["client_name"] = "KiCad";
1115 reply["client_version"] = GetSemanticVersion().ToStdString();
1116 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
1117 sendRpcMessage( command, std::move( reply ), messageId );
1118 return;
1119 }
1120 else if( command == wxS( "GET_KICAD_VERSION" ) )
1121 {
1122 nlohmann::json reply = nlohmann::json::object();
1123 reply["kicad_version"] = GetSemanticVersion().ToStdString();
1124 sendRpcMessage( command, std::move( reply ), messageId );
1125 return;
1126 }
1127 else if( command == wxS( "LIST_SUPPORTED_VERSIONS" ) )
1128 {
1129 nlohmann::json reply = nlohmann::json::object();
1130 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
1131 sendRpcMessage( command, std::move( reply ), messageId );
1132 return;
1133 }
1134 else if( command == wxS( "CAPABILITIES" ) )
1135 {
1136 nlohmann::json reply = nlohmann::json::object();
1137 reply["commands"] = { "NEW_SESSION", "GET_KICAD_VERSION", "LIST_SUPPORTED_VERSIONS",
1138 "CAPABILITIES", "PING", "PONG", "REMOTE_LOGIN", "LOGOUT", "DL_SYMBOL", "DL_COMPONENT", "DL_FOOTPRINT",
1139 "DL_SPICE", "DL_3DMODEL" };
1140 reply["compression"] = { "NONE", "ZSTD" };
1141 reply["max_message_size"] = 0;
1142 sendRpcMessage( command, std::move( reply ), messageId );
1143 return;
1144 }
1145 else if( command == wxS( "PING" ) )
1146 {
1147 nlohmann::json reply = nlohmann::json::object();
1148
1149 if( params.contains( "nonce" ) )
1150 reply["nonce"] = params["nonce"];
1151
1152 sendRpcMessage( wxS( "PONG" ), std::move( reply ), messageId );
1153 return;
1154 }
1155 else if( command == wxS( "PONG" ) )
1156 {
1157 return;
1158 }
1159 else if( command == wxS( "REMOTE_LOGIN" ) )
1160 {
1161 handleRemoteLogin( params, messageId );
1162 return;
1163 }
1164 else if( command == wxS( "LOGOUT" ) )
1165 {
1166 m_activeUserId.Clear();
1167
1168 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
1169 {
1170 if( !m_activeDataSourceUrl.IsEmpty() )
1171 {
1172 settings->m_RemoteSymbol.user_ids.erase( m_activeDataSourceUrl );
1173 Pgm().GetSettingsManager().Save( settings );
1174 }
1175 }
1176
1177 sendRpcMessage( command, nlohmann::json::object(), messageId );
1179 return;
1180 }
1181
1182 const wxString compression = jsonString( params, "compression" );
1183
1184 if( command.StartsWith( wxS( "DL_" ) ) )
1185 {
1186 if( compression.IsEmpty() )
1187 {
1188 respondWithError( command, messageId, wxS( "INVALID_PARAMETERS" ), _( "Missing compression metadata." ) );
1189 return;
1190 }
1191
1192 std::vector<uint8_t> decoded;
1193 wxString error;
1194
1195 if( !decodeBase64Payload( data, decoded, error ) )
1196 {
1197 respondWithError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
1198 return;
1199 }
1200
1201 std::vector<uint8_t> payload;
1202 wxScopedCharBuffer compUtf8 = compression.ToUTF8();
1203 std::string compressionStr = compUtf8 ? std::string( compUtf8.data() ) : std::string();
1204
1205 if( !decompressIfNeeded( compressionStr, decoded, payload, error ) )
1206 {
1207 respondWithError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
1208 return;
1209 }
1210
1211 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "handleRpcMessage: decoded size=%zu decompressed size=%zu command=%s",
1212 decoded.size(), payload.size(), command.ToUTF8().data() );
1213
1214 nlohmann::json responseParams = nlohmann::json::object();
1215 bool ok = false;
1216
1217 if( command == wxS( "DL_SYMBOL" ) )
1218 ok = receiveSymbol( params, payload, error );
1219 else if( command == wxS( "DL_COMPONENT" ) )
1220 ok = receiveComponent( params, payload, error, &responseParams );
1221 else if( command == wxS( "DL_FOOTPRINT" ) )
1222 ok = receiveFootprint( params, payload, error );
1223 else if( command == wxS( "DL_3DMODEL" ) )
1224 ok = receive3DModel( params, payload, error );
1225 else if( command == wxS( "DL_SPICE" ) )
1226 ok = receiveSPICEModel( params, payload, error );
1227 else
1228 {
1229 respondWithError( command, messageId, wxS( "UNKNOWN_COMMAND" ),
1230 wxString::Format( _( "Command '%s' is not supported." ), command ) );
1231 return;
1232 }
1233
1234 if( ok )
1235 sendRpcMessage( command, std::move( responseParams ), messageId );
1236 else
1237 respondWithError( command, messageId, wxS( "INTERNAL_ERROR" ),
1238 error.IsEmpty() ? _( "Unable to store payload." ) : error );
1239
1240 return;
1241 }
1242
1243 respondWithError( command, messageId, wxS( "UNKNOWN_COMMAND" ),
1244 wxString::Format( _( "Command '%s' is not supported." ), command ) );
1245}
1246
1247
1248bool PANEL_REMOTE_SYMBOL::ensureDestinationRoot( wxFileName& aOutDir, wxString& aError ) const
1249{
1250 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1251
1252 if( !settings )
1253 {
1254 aError = _( "Unable to load schematic settings." );
1255 return false;
1256 }
1257
1258 wxString destination = settings->m_RemoteSymbol.destination_dir;
1259
1260 if( destination.IsEmpty() )
1262
1263 destination = ExpandEnvVarSubstitutions( destination,
1264 &Pgm().GetSettingsManager().Prj() );
1265 destination.Trim( true ).Trim( false );
1266
1267 if( destination.IsEmpty() )
1268 {
1269 aError = _( "Destination directory is not configured." );
1270 return false;
1271 }
1272
1273 wxFileName dir = wxFileName::DirName( destination );
1274 dir.Normalize( FN_NORMALIZE_FLAGS );
1275
1276 if( !dir.DirExists() )
1277 {
1278 if( !dir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1279 {
1280 aError = wxString::Format( _( "Unable to create directory '%s'." ), dir.GetFullPath() );
1281 return false;
1282 }
1283 }
1284
1285 aOutDir = dir;
1286 return true;
1287}
1288
1289
1290bool PANEL_REMOTE_SYMBOL::ensureSymbolLibraryEntry( const wxFileName& aLibraryFile,
1291 const wxString& aNickname, bool aGlobalTable,
1292 wxString& aError ) const
1293{
1294 LIBRARY_MANAGER& manager = Pgm().GetLibraryManager();
1295 std::optional<LIBRARY_TABLE*> tableOpt = manager.Table( LIBRARY_TABLE_TYPE::SYMBOL,
1297
1298 if( !tableOpt )
1299 {
1300 aError = _( "Unable to access the symbol library table." );
1301 return false;
1302 }
1303
1304 LIBRARY_TABLE* table = *tableOpt;
1305 const wxString fullPath = aLibraryFile.GetFullPath();
1306
1307 if( table->HasRow( aNickname ) )
1308 {
1309 if( std::optional<LIBRARY_TABLE_ROW*> rowOpt = table->Row( aNickname ); rowOpt )
1310 {
1311 LIBRARY_TABLE_ROW* row = *rowOpt;
1312
1313 if( row->URI() != fullPath )
1314 {
1315 row->SetURI( fullPath );
1316
1317 if( !table->Save() )
1318 {
1319 aError = _( "Failed to update the symbol library table." );
1320 return false;
1321 }
1322 }
1323 }
1324
1325 return true;
1326 }
1327
1328 LIBRARY_TABLE_ROW& row = table->InsertRow();
1329 row.SetNickname( aNickname );
1330 row.SetURI( fullPath );
1331 row.SetType( wxS( "KiCad" ) );
1332 row.SetOptions( wxString() );
1333 row.SetDescription( _( "Remote download" ) );
1334 row.SetOk( true );
1335
1336 if( !table->Save() )
1337 {
1338 aError = _( "Failed to save the symbol library table." );
1339 return false;
1340 }
1341
1342 return true;
1343}
1344
1345
1346bool PANEL_REMOTE_SYMBOL::ensureFootprintLibraryEntry( const wxFileName& aLibraryDir,
1347 const wxString& aNickname,
1348 bool aGlobalTable, wxString& aError ) const
1349{
1350 LIBRARY_MANAGER& manager = Pgm().GetLibraryManager();
1351 std::optional<LIBRARY_TABLE*> tableOpt = manager.Table( LIBRARY_TABLE_TYPE::FOOTPRINT,
1353
1354 if( !tableOpt )
1355 {
1356 aError = _( "Unable to access the footprint library table." );
1357 return false;
1358 }
1359
1360 LIBRARY_TABLE* table = *tableOpt;
1361 const wxString fullPath = aLibraryDir.GetFullPath();
1362
1363 if( table->HasRow( aNickname ) )
1364 {
1365 if( std::optional<LIBRARY_TABLE_ROW*> rowOpt = table->Row( aNickname ); rowOpt )
1366 {
1367 LIBRARY_TABLE_ROW* row = *rowOpt;
1368
1369 if( row->URI() != fullPath )
1370 {
1371 row->SetURI( fullPath );
1372
1373 if( !table->Save() )
1374 {
1375 aError = _( "Failed to update the footprint library table." );
1376 return false;
1377 }
1378 }
1379 }
1380
1381 return true;
1382 }
1383
1384 LIBRARY_TABLE_ROW& row = table->InsertRow();
1385 row.SetNickname( aNickname );
1386 row.SetURI( fullPath );
1387 row.SetType( wxS( "KiCad" ) );
1388 row.SetOptions( wxString() );
1389 row.SetDescription( _( "Remote download" ) );
1390 row.SetOk( true );
1391
1392 if( !table->Save() )
1393 {
1394 aError = _( "Failed to save the footprint library table." );
1395 return false;
1396 }
1397
1398 return true;
1399}
1400
1401
1403{
1404 wxString prefix;
1405
1406 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
1407 prefix = settings->m_RemoteSymbol.library_prefix;
1408
1409 if( prefix.IsEmpty() )
1411
1412 prefix.Trim( true ).Trim( false );
1413
1414 if( prefix.IsEmpty() )
1415 prefix = wxS( "remote" );
1416
1417 for( size_t i = 0; i < prefix.length(); ++i )
1418 {
1419 wxUniChar ch = prefix[i];
1420
1421 if( !( wxIsalnum( ch ) || ch == '_' || ch == '-' ) )
1422 prefix[i] = '_';
1423 }
1424
1425 return prefix;
1426}
1427
1428
1429bool PANEL_REMOTE_SYMBOL::placeDownloadedSymbol( const wxString& aNickname,
1430 const wxString& aLibItemName,
1431 wxString& aError )
1432{
1433 if( !m_frame )
1434 {
1435 aError = _( "No schematic editor is available for placement." );
1436 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: no frame available" );
1437 return false;
1438 }
1439
1440 if( aNickname.IsEmpty() || aLibItemName.IsEmpty() )
1441 {
1442 aError = _( "Downloaded symbol metadata is incomplete." );
1443 return false;
1444 }
1445
1446 LIB_ID libId;
1447 libId.SetLibNickname( aNickname );
1448 libId.SetLibItemName( aLibItemName );
1449
1450 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1451 "placeDownloadedSymbol: attempting GetLibSymbol for %s:%s",
1452 aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() );
1453
1454 // Ensure the library adapter has loaded this library
1456 if( adapter )
1457 {
1458 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1459 "placeDownloadedSymbol: got adapter, attempting LoadSymbol" );
1460 LIB_SYMBOL* adapterSymbol = adapter->LoadSymbol( aNickname, aLibItemName );
1461 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1462 "placeDownloadedSymbol: adapter LoadSymbol returned %s",
1463 adapterSymbol ? "valid symbol" : "nullptr" );
1464 }
1465
1466 LIB_SYMBOL* libSymbol = m_frame->GetLibSymbol( libId );
1467
1468 if( !libSymbol )
1469 {
1470 aError = _( "Unable to load the downloaded symbol for placement." );
1471 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: failed to load libSymbol %s:%s",
1472 aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() );
1473 return false;
1474 }
1475
1476 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: loaded libSymbol %s:%s",
1477 aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() );
1478
1479 SCH_SYMBOL* symbol = new SCH_SYMBOL( *libSymbol, libId, &m_frame->GetCurrentSheet(), 1 );
1480 symbol->SetParent( m_frame->GetScreen() );
1481
1482 if( EESCHEMA_SETTINGS* cfg = m_frame->eeconfig(); cfg && cfg->m_AutoplaceFields.enable )
1483 {
1484 symbol->AutoplaceFields( nullptr, AUTOPLACE_AUTO );
1485 }
1486
1487 TOOL_MANAGER* toolMgr = m_frame->GetToolManager();
1488
1489 if( !toolMgr )
1490 {
1491 delete symbol;
1492 aError = _( "Unable to access the schematic placement tools." );
1493 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: no tool manager available" );
1494 return false;
1495 }
1496
1497 m_frame->Raise();
1499 SCH_ACTIONS::PLACE_SYMBOL_PARAMS{ symbol, true } );
1500 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: posted placeSymbol action for %s:%s",
1501 aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() );
1502 return true;
1503}
1504
1505
1506bool PANEL_REMOTE_SYMBOL::receiveSymbol( const nlohmann::json& aParams,
1507 const std::vector<uint8_t>& aPayload,
1508 wxString& aError )
1509{
1510 const wxString mode = jsonString( aParams, "mode" );
1511 const bool placeAfterDownload = mode.IsSameAs( wxS( "PLACE" ), false );
1512
1513 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false )
1514 && !mode.IsSameAs( wxS( "PLACE" ), false ) )
1515 {
1516 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1517 return false;
1518 }
1519
1520 const wxString contentType = jsonString( aParams, "content_type" );
1521
1522 if( !contentType.IsSameAs( wxS( "KICAD_SYMBOL_V1" ), false ) )
1523 {
1524 aError = _( "Unsupported symbol payload type." );
1525 return false;
1526 }
1527
1528 if( !m_frame )
1529 {
1530 aError = _( "No schematic editor is available to store symbols." );
1531 return false;
1532 }
1533
1534 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1535
1536 if( !settings )
1537 {
1538 aError = _( "Unable to load schematic settings." );
1539 return false;
1540 }
1541
1542 const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table;
1543
1544 wxFileName baseDir;
1545
1546 if( !ensureDestinationRoot( baseDir, aError ) )
1547 return false;
1548
1549 wxFileName symDir = baseDir;
1550 symDir.AppendDir( wxS( "symbols" ) );
1551
1552 if( !symDir.DirExists() && !symDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1553 {
1554 aError = wxString::Format( _( "Unable to create '%s'." ), symDir.GetFullPath() );
1555 return false;
1556 }
1557
1558 wxString libItemName = jsonString( aParams, "name" );
1559 wxString baseName = libItemName;
1560
1561 if( baseName.IsEmpty() )
1562 baseName = jsonString( aParams, "library" );
1563
1564 if( baseName.IsEmpty() )
1565 baseName = wxS( "symbol" );
1566
1567 baseName.Trim( true ).Trim( false );
1568
1569 if( libItemName.IsEmpty() )
1570 libItemName = baseName;
1571
1572 wxString sanitizedName = sanitizeFileComponent( baseName, wxS( "symbol" ) );
1573
1574 if( libItemName.IsEmpty() )
1575 libItemName = sanitizedName;
1576
1577 const wxString nickname = sanitizedPrefix() + wxS( "_sym_" ) + sanitizedName;
1578
1579 wxFileName outFile( symDir );
1580 outFile.SetFullName( nickname + wxS( ".kicad_sym" ) );
1581
1582 if( !ensureSymbolLibraryEntry( outFile, nickname, addToGlobal, aError ) )
1583 return false;
1584
1586
1587 if( !adapter )
1588 {
1589 aError = _( "Unable to access the symbol library manager." );
1590 return false;
1591 }
1592
1593 // Ensure the adapter has loaded this library (it was just added to the table)
1594 std::optional<LIB_STATUS> libStatus = adapter->LoadOne( nickname );
1595 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1596 "receiveSymbol: LoadOne(%s) returned %s",
1597 nickname.ToUTF8().data(),
1598 libStatus.has_value() ? "valid status" : "nullopt" );
1599
1600 std::unique_ptr<LIB_SYMBOL> downloadedSymbol = loadSymbolFromPayload( aPayload, libItemName, aError );
1601
1602 if( !downloadedSymbol )
1603 {
1604 if( aError.IsEmpty() )
1605 aError = _( "Unable to parse the downloaded symbol." );
1606
1607 return false;
1608 }
1609
1610 downloadedSymbol->SetName( libItemName );
1611
1612 LIB_ID savedId;
1613 savedId.SetLibNickname( nickname );
1614 savedId.SetLibItemName( libItemName );
1615 downloadedSymbol->SetLibId( savedId );
1616
1617 if( adapter->SaveSymbol( nickname, downloadedSymbol.get(), true )
1619 {
1620 aError = _( "Unable to save the downloaded symbol." );
1621 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1622 "receiveSymbol: failed to save symbol %s to library %s",
1623 libItemName.ToUTF8().data(), nickname.ToUTF8().data() );
1624 return false;
1625 }
1626
1627 // SaveSymbol transfers ownership to the cache, so release our unique_ptr
1628 downloadedSymbol.release();
1629
1630 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ),
1631 "receiveSymbol: saved symbol %s into library %s", libItemName.ToUTF8().data(),
1632 nickname.ToUTF8().data() );
1633
1634 const LIBRARY_TABLE_SCOPE scope = addToGlobal ? LIBRARY_TABLE_SCOPE::GLOBAL
1636
1637 // Place before forcing a library reload so we can use the existing in-memory cache.
1638 // Reloading first invalidates the adapter's cache, causing GetLibSymbol() to fail
1639 // because LoadSymbol() does not auto-load libraries (it only fetches if already loaded).
1640 if( placeAfterDownload )
1641 {
1642 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSymbol: placing symbol now (nickname=%s libItem=%s)",
1643 nickname.ToUTF8().data(), libItemName.ToUTF8().data() );
1644 bool placedOk = placeDownloadedSymbol( nickname, libItemName, aError );
1645
1646 // Perform the reload afterwards so subsequent operations see the fresh library on disk.
1648 return placedOk;
1649 }
1650
1651 // No immediate placement requested; safe to reload now.
1653
1654 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSymbol: saved symbol (nickname=%s libItem=%s)" ,
1655 nickname.ToUTF8().data(), libItemName.ToUTF8().data() );
1656
1657 return true;
1658}
1659
1660
1661bool PANEL_REMOTE_SYMBOL::receiveFootprint( const nlohmann::json& aParams,
1662 const std::vector<uint8_t>& aPayload,
1663 wxString& aError )
1664{
1665 const wxString mode = jsonString( aParams, "mode" );
1666
1667 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveFootprint: mode=%s", mode.ToUTF8().data() );
1668
1669 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false )
1670 && !mode.IsSameAs( wxS( "PLACE" ), false ) )
1671 {
1672 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1673 return false;
1674 }
1675
1676 const wxString contentType = jsonString( aParams, "content_type" );
1677
1678 if( !contentType.IsSameAs( wxS( "KICAD_FOOTPRINT_V1" ), false ) )
1679 {
1680 aError = _( "Unsupported footprint payload type." );
1681 return false;
1682 }
1683
1684 wxFileName baseDir;
1685
1686 if( !ensureDestinationRoot( baseDir, aError ) )
1687 return false;
1688
1689 wxFileName fpRoot = baseDir;
1690 fpRoot.AppendDir( wxS( "footprints" ) );
1691
1692 if( !fpRoot.DirExists() && !fpRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1693 {
1694 aError = wxString::Format( _( "Unable to create '%s'." ), fpRoot.GetFullPath() );
1695 return false;
1696 }
1697
1698 wxString footprintName = jsonString( aParams, "name" );
1699
1700 if( footprintName.IsEmpty() )
1701 footprintName = sanitizedPrefix() + wxS( "_footprint" );
1702
1703 footprintName = sanitizeFileComponent( footprintName, sanitizedPrefix() + wxS( "_footprint" ) );
1704
1705 wxString libNickname = sanitizedPrefix() + wxS( "_fp_" ) + footprintName;
1706
1707 wxFileName libDir = fpRoot;
1708 libDir.AppendDir( libNickname + wxS( ".pretty" ) );
1709
1710 if( !libDir.DirExists() && !libDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1711 {
1712 aError = wxString::Format( _( "Unable to create '%s'." ), libDir.GetFullPath() );
1713 return false;
1714 }
1715
1716 wxString fileName = footprintName;
1717
1718 if( !fileName.Lower().EndsWith( wxS( ".kicad_mod" ) ) )
1719 fileName += wxS( ".kicad_mod" );
1720
1721 wxFileName outFile( libDir );
1722 outFile.SetFullName( fileName );
1723
1724 if( !writeBinaryFile( outFile, aPayload, aError ) )
1725 return false;
1726
1727 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveFootprint: wrote footprint %s in lib %s",
1728 outFile.GetFullPath(), libNickname.ToUTF8().data() );
1729
1730 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1731
1732 if( !settings )
1733 {
1734 aError = _( "Unable to load schematic settings." );
1735 return false;
1736 }
1737
1738 const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table;
1739
1740 if( !ensureFootprintLibraryEntry( libDir, libNickname, addToGlobal, aError ) )
1741 return false;
1742
1743 const LIBRARY_TABLE_SCOPE scope = addToGlobal ? LIBRARY_TABLE_SCOPE::GLOBAL
1746
1747 return true;
1748}
1749
1750
1751bool PANEL_REMOTE_SYMBOL::receive3DModel( const nlohmann::json& aParams,
1752 const std::vector<uint8_t>& aPayload,
1753 wxString& aError )
1754{
1755 const wxString mode = jsonString( aParams, "mode" );
1756
1757 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receive3DModel: mode=%s", mode.ToUTF8().data() );
1758
1759 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false )
1760 && !mode.IsSameAs( wxS( "PLACE" ), false ) )
1761 {
1762 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1763 return false;
1764 }
1765
1766 const wxString contentType = jsonString( aParams, "content_type" );
1767
1768 if( !contentType.IsSameAs( wxS( "KICAD_3D_MODEL_STEP" ), false ) &&
1769 !contentType.IsSameAs( wxS( "KICAD_3D_MODEL_WRL" ), false ) )
1770 {
1771 aError = _( "Unsupported 3D model payload type." );
1772 return false;
1773 }
1774
1775 wxFileName baseDir;
1776
1777 if( !ensureDestinationRoot( baseDir, aError ) )
1778 return false;
1779
1780 wxFileName modelDir = baseDir;
1781 modelDir.AppendDir( sanitizedPrefix() + wxS( "_3d" ) );
1782
1783 if( !modelDir.DirExists() && !modelDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1784 {
1785 aError = wxString::Format( _( "Unable to create '%s'." ), modelDir.GetFullPath() );
1786 return false;
1787 }
1788
1789 wxString fileName = jsonString( aParams, "name" );
1790
1791 if( fileName.IsEmpty() )
1792 fileName = sanitizedPrefix() + wxS( "_model" );
1793
1794 fileName = sanitizeFileComponent( fileName, sanitizedPrefix() + wxS( "_model" ) );
1795
1796 wxString extension = wxS( ".step" );
1797
1798 if( contentType.IsSameAs( wxS( "KICAD_3D_MODEL_WRL" ), false ) )
1799 extension = wxS( ".wrl" );
1800
1801 if( !fileName.Lower().EndsWith( extension ) )
1802 fileName += extension;
1803
1804 wxFileName outFile( modelDir );
1805 outFile.SetFullName( fileName );
1806
1807 bool ok = writeBinaryFile( outFile, aPayload, aError );
1808
1809 if( ok )
1810 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receive3DModel: wrote model %s", outFile.GetFullPath() );
1811
1812 return ok;
1813}
1814
1815
1816bool PANEL_REMOTE_SYMBOL::receiveSPICEModel( const nlohmann::json& aParams,
1817 const std::vector<uint8_t>& aPayload,
1818 wxString& aError )
1819{
1820 const wxString mode = jsonString( aParams, "mode" );
1821
1822 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSPICEModel: mode=%s", mode.ToUTF8().data() );
1823
1824 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false )
1825 && !mode.IsSameAs( wxS( "PLACE" ), false ) )
1826 {
1827 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1828 return false;
1829 }
1830
1831 const wxString contentType = jsonString( aParams, "content_type" );
1832
1833 if( !contentType.IsSameAs( wxS( "KICAD_SPICE_MODEL_V1" ), false ) )
1834 {
1835 aError = _( "Unsupported SPICE payload type." );
1836 return false;
1837 }
1838
1839 wxFileName baseDir;
1840
1841 if( !ensureDestinationRoot( baseDir, aError ) )
1842 return false;
1843
1844 wxFileName spiceDir = baseDir;
1845 spiceDir.AppendDir( sanitizedPrefix() + wxS( "_spice" ) );
1846
1847 if( !spiceDir.DirExists() && !spiceDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1848 {
1849 aError = wxString::Format( _( "Unable to create '%s'." ), spiceDir.GetFullPath() );
1850 return false;
1851 }
1852
1853 wxString fileName = jsonString( aParams, "name" );
1854
1855 if( fileName.IsEmpty() )
1856 fileName = sanitizedPrefix() + wxS( "_model.cir" );
1857
1858 fileName = sanitizeFileComponent( fileName, sanitizedPrefix() + wxS( "_model.cir" ) );
1859
1860 if( !fileName.Lower().EndsWith( wxS( ".cir" ) ) )
1861 fileName += wxS( ".cir" );
1862
1863 wxFileName outFile( spiceDir );
1864 outFile.SetFullName( fileName );
1865
1866 bool ok = writeBinaryFile( outFile, aPayload, aError );
1867
1868 if( ok )
1869 wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSPICEModel: wrote spice model %s", outFile.GetFullPath() );
1870
1871 return ok;
1872}
1873
1874
1875bool PANEL_REMOTE_SYMBOL::receiveComponent( const nlohmann::json& aParams,
1876 const std::vector<uint8_t>& aPayload,
1877 wxString& aError, nlohmann::json* aResponseParams )
1878{
1879 nlohmann::json components;
1880
1881 try
1882 {
1883 components = nlohmann::json::parse( aPayload.begin(), aPayload.end() );
1884 }
1885 catch( const std::exception& e )
1886 {
1887 aError = wxString::Format( _( "Failed to parse component list: %s" ), e.what() );
1888 return false;
1889 }
1890
1891 if( !components.is_array() )
1892 {
1893 aError = _( "Component list must be an array." );
1894 return false;
1895 }
1896
1897 if( components.empty() )
1898 {
1899 aError = _( "Component list was empty." );
1900 return false;
1901 }
1902
1903 wxString libNickname = sanitizeFileComponent( wxString::FromUTF8( aParams.value( "library", "" ) ),
1904 wxS( "Remote" ) );
1905
1906 if( libNickname.IsEmpty() )
1907 libNickname = wxS( "Remote" );
1908
1909 wxFileName baseDir;
1910
1911 if( !ensureDestinationRoot( baseDir, aError ) )
1912 return false;
1913
1914 auto ensureDirectory = [&]( wxFileName& aDir ) -> bool
1915 {
1916 if( aDir.DirExists() )
1917 return true;
1918
1919 if( !aDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
1920 {
1921 aError = wxString::Format( _( "Unable to create '%s'." ), aDir.GetFullPath() );
1922 return false;
1923 }
1924
1925 return true;
1926 };
1927
1928 wxFileName librariesDir( baseDir );
1929 librariesDir.AppendDir( wxS( "symbols" ) );
1930
1931 if( !ensureDirectory( librariesDir ) )
1932 return false;
1933
1934 wxFileName symbolLib( librariesDir );
1935 symbolLib.SetFullName( libNickname + wxS( ".kicad_sym" ) );
1936
1937 wxFileName footprintsDir( baseDir );
1938 footprintsDir.AppendDir( wxS( "footprints" ) );
1939
1940 if( !ensureDirectory( footprintsDir ) )
1941 return false;
1942
1943 wxFileName footprintLibDir( footprintsDir );
1944 footprintLibDir.AppendDir( libNickname + wxS( ".pretty" ) );
1945
1946 if( !ensureDirectory( footprintLibDir ) )
1947 return false;
1948
1949 wxFileName modelDir( baseDir );
1950 modelDir.AppendDir( wxS( "3dmodels" ) );
1951
1952 if( !ensureDirectory( modelDir ) )
1953 return false;
1954
1955 modelDir.AppendDir( libNickname );
1956
1957 if( !ensureDirectory( modelDir ) )
1958 return false;
1959
1960 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1961
1962 if( !settings )
1963 {
1964 aError = _( "Unable to load schematic settings." );
1965 return false;
1966 }
1967
1968 const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table;
1969
1970 std::unique_ptr<SCH_IO> symbolPlugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
1971
1972 if( !symbolPlugin )
1973 {
1974 aError = _( "Unable to access the KiCad symbol plugin." );
1975 return false;
1976 }
1977
1978 struct COMPONENT_PAYLOAD
1979 {
1980 std::string type;
1981 wxString declaredName;
1982 wxString sanitizedName;
1983 wxString finalName;
1984 wxString payloadSymbolName;
1985 std::string checksum;
1986 std::vector<uint8_t> content;
1987 bool skip = false;
1988 };
1989
1990 std::vector<COMPONENT_PAYLOAD> prepared;
1991 prepared.reserve( components.size() );
1992
1993 for( const nlohmann::json& component : components )
1994 {
1995 if( !component.is_object() )
1996 {
1997 aError = _( "Component entries must be objects." );
1998 return false;
1999 }
2000
2001 COMPONENT_PAYLOAD entry;
2002 entry.type = component.value( "type", "" );
2003
2004 if( entry.type.empty() )
2005 {
2006 aError = _( "Component entry was missing a type." );
2007 return false;
2008 }
2009
2010 std::transform( entry.type.begin(), entry.type.end(), entry.type.begin(),
2011 []( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
2012
2013 entry.declaredName = wxString::FromUTF8( component.value( "name", entry.type ).c_str() );
2014 entry.declaredName.Trim( true ).Trim( false );
2015
2016 wxString fallback = sanitizedPrefix() + wxS( "_" ) + wxString::FromUTF8( entry.type );
2017 entry.sanitizedName = sanitizeFileComponent( entry.declaredName, fallback );
2018
2019 if( entry.sanitizedName.IsEmpty() )
2020 entry.sanitizedName = fallback;
2021
2022 entry.finalName = entry.sanitizedName;
2023 entry.payloadSymbolName = entry.declaredName.IsEmpty() ? entry.sanitizedName : entry.declaredName;
2024
2025 const std::string encoded = component.value( "content", std::string() );
2026
2027 if( !decodeBase64Payload( encoded, entry.content, aError ) )
2028 return false;
2029
2030 const std::string compression = component.value( "compression", std::string() );
2031
2032 if( !compression.empty() && compression != "NONE" )
2033 {
2034 std::vector<uint8_t> decompressed;
2035
2036 if( !decompressIfNeeded( compression, entry.content, decompressed, aError ) )
2037 return false;
2038
2039 entry.content = std::move( decompressed );
2040 }
2041
2042 entry.checksum = component.value( "checksum", std::string() );
2043
2044 if( entry.checksum.empty() )
2045 entry.checksum = HashBuffer( entry.content );
2046
2047 prepared.emplace_back( std::move( entry ) );
2048 }
2049
2050 nlohmann::json skipped = nlohmann::json::array();
2051 nlohmann::json renamedReport = nlohmann::json::array();
2052 std::map<std::string, std::map<wxString, wxString>> renames;
2053 std::set<wxString> reservedSymbolNames;
2054 std::set<wxString> reservedFootprintNames;
2055 std::set<wxString> reservedModelNames;
2056
2057 auto recordRename = [&]( const std::string& aType, const wxString& aFrom, const wxString& aTo )
2058 {
2059 if( aFrom == aTo )
2060 return;
2061
2062 renames[aType][aFrom] = aTo;
2063
2064 renamedReport.push_back( { { "type", aType },
2065 { "from", aFrom.ToStdString() },
2066 { "to", aTo.ToStdString() } } );
2067 };
2068
2069 auto recordSkip = [&]( const std::string& aType, const wxString& aName )
2070 {
2071 skipped.push_back( { { "type", aType }, { "name", aName.ToStdString() } } );
2072 };
2073
2074 auto footprintPathFor = [&]( const wxString& aName )
2075 {
2076 wxFileName fn( footprintLibDir );
2077 fn.SetFullName( aName + wxS( ".kicad_mod" ) );
2078 return fn;
2079 };
2080
2081 auto modelPathFor = [&]( const wxString& aName )
2082 {
2083 wxFileName fn( modelDir );
2084 fn.SetFullName( aName );
2085 return fn;
2086 };
2087
2088 for( COMPONENT_PAYLOAD& entry : prepared )
2089 {
2090 if( entry.type == "footprint" )
2091 {
2092 wxFileName existing = footprintPathFor( entry.sanitizedName );
2093 std::string localChecksum;
2094
2095 if( HashFile( existing, localChecksum ) && localChecksum == entry.checksum )
2096 {
2097 entry.skip = true;
2098 recordSkip( entry.type, entry.sanitizedName );
2099 continue;
2100 }
2101
2102 wxString candidate = entry.sanitizedName;
2103 int suffix = 1;
2104
2105 auto collides = [&]( const wxString& name )
2106 {
2107 if( reservedFootprintNames.contains( name ) )
2108 return true;
2109
2110 return footprintPathFor( name ).FileExists();
2111 };
2112
2113 while( collides( candidate ) )
2114 candidate = AppendNumericSuffix( entry.sanitizedName, suffix++ );
2115
2116 reservedFootprintNames.insert( candidate );
2117 recordRename( entry.type, entry.sanitizedName, candidate );
2118 entry.finalName = candidate;
2119 }
2120 else if( entry.type == "3dmodel" )
2121 {
2122 wxFileName existing = modelPathFor( entry.sanitizedName );
2123 std::string localChecksum;
2124
2125 if( HashFile( existing, localChecksum ) && localChecksum == entry.checksum )
2126 {
2127 entry.skip = true;
2128 recordSkip( entry.type, entry.sanitizedName );
2129 continue;
2130 }
2131
2132 wxString candidate = entry.sanitizedName;
2133 int suffix = 1;
2134
2135 auto collides = [&]( const wxString& name )
2136 {
2137 if( reservedModelNames.contains( name ) )
2138 return true;
2139
2140 return modelPathFor( name ).FileExists();
2141 };
2142
2143 while( collides( candidate ) )
2144 candidate = AppendNumericSuffixToFilename( entry.sanitizedName, suffix++ );
2145
2146 reservedModelNames.insert( candidate );
2147 recordRename( entry.type, entry.sanitizedName, candidate );
2148 entry.finalName = candidate;
2149 }
2150 else if( entry.type == "symbol" )
2151 {
2152 std::string localChecksum;
2153
2154 if( ComputeSymbolChecksum( symbolPlugin.get(), symbolLib, entry.sanitizedName, localChecksum )
2155 && localChecksum == entry.checksum )
2156 {
2157 entry.skip = true;
2158 recordSkip( entry.type, entry.sanitizedName );
2159 continue;
2160 }
2161
2162 wxString candidate = entry.sanitizedName;
2163 int suffix = 1;
2164
2165 auto exists = [&]( const wxString& name )
2166 {
2167 if( reservedSymbolNames.contains( name ) )
2168 return true;
2169
2170 std::unique_ptr<LIB_SYMBOL> symbol = TryCloneSymbol( symbolPlugin.get(), symbolLib, name );
2171 return static_cast<bool>( symbol );
2172 };
2173
2174 while( exists( candidate ) )
2175 candidate = AppendNumericSuffix( entry.sanitizedName, suffix++ );
2176
2177 reservedSymbolNames.insert( candidate );
2178 recordRename( entry.type, entry.sanitizedName, candidate );
2179 entry.finalName = candidate;
2180 }
2181 else
2182 {
2183 aError = wxString::Format( _( "Unsupported component type '%s'." ),
2184 wxString::FromUTF8( entry.type ) );
2185 return false;
2186 }
2187 }
2188
2189 bool footprintLibraryReady = false;
2190 bool symbolLibraryReady = false;
2191
2192 auto ensureFootprintLibrary = [&]() -> bool
2193 {
2194 if( footprintLibraryReady )
2195 return true;
2196
2197 if( !ensureFootprintLibraryEntry( footprintLibDir, libNickname, addToGlobal, aError ) )
2198 return false;
2199
2200 footprintLibraryReady = true;
2201 return true;
2202 };
2203
2204 auto ensureSymbolLibrary = [&]() -> bool
2205 {
2206 if( symbolLibraryReady )
2207 return true;
2208
2209 if( !ensureSymbolLibraryEntry( symbolLib, libNickname, addToGlobal, aError ) )
2210 return false;
2211
2212 symbolLibraryReady = true;
2213 return true;
2214 };
2215
2216 for( COMPONENT_PAYLOAD& entry : prepared )
2217 {
2218 if( entry.skip )
2219 continue;
2220
2221 if( entry.type == "footprint" )
2222 {
2223 if( !ensureFootprintLibrary() )
2224 return false;
2225
2226 if( !renames["3dmodel"].empty() )
2227 {
2228 std::string contentStr( entry.content.begin(), entry.content.end() );
2229
2230 for( const auto& [oldModel, newModel] : renames["3dmodel"] )
2231 {
2232 const std::string from = oldModel.ToStdString();
2233 const std::string to = newModel.ToStdString();
2234 size_t pos = contentStr.find( from );
2235
2236 while( pos != std::string::npos )
2237 {
2238 contentStr.replace( pos, from.size(), to );
2239 pos = contentStr.find( from, pos + to.size() );
2240 }
2241 }
2242
2243 entry.content.assign( contentStr.begin(), contentStr.end() );
2244 }
2245
2246 wxFileName target = footprintPathFor( entry.finalName );
2247
2248 if( !writeBinaryFile( target, entry.content, aError ) )
2249 return false;
2250 }
2251 else if( entry.type == "3dmodel" )
2252 {
2253 wxFileName target = modelPathFor( entry.finalName );
2254
2255 if( !target.GetPath().IsEmpty() )
2256 {
2257 wxFileName parent( target.GetPath(), wxEmptyString );
2258
2259 if( !parent.DirExists() && !parent.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2260 {
2261 aError = wxString::Format( _( "Unable to create '%s'." ), parent.GetFullPath() );
2262 return false;
2263 }
2264 }
2265
2266 if( !writeBinaryFile( target, entry.content, aError ) )
2267 return false;
2268 }
2269 else if( entry.type == "symbol" )
2270 {
2271 if( !ensureSymbolLibrary() )
2272 return false;
2273
2274 if( !renames["footprint"].empty() )
2275 {
2276 std::string contentStr( entry.content.begin(), entry.content.end() );
2277
2278 for( const auto& [oldFp, newFp] : renames["footprint"] )
2279 {
2280 const std::string from = oldFp.ToStdString();
2281 const std::string to = newFp.ToStdString();
2282 size_t pos = contentStr.find( from );
2283
2284 while( pos != std::string::npos )
2285 {
2286 contentStr.replace( pos, from.size(), to );
2287 pos = contentStr.find( from, pos + to.size() );
2288 }
2289 }
2290
2291 entry.content.assign( contentStr.begin(), contentStr.end() );
2292 }
2293
2294 std::unique_ptr<LIB_SYMBOL> symbol = loadSymbolFromPayload( entry.content,
2295 entry.payloadSymbolName,
2296 aError );
2297
2298 if( !symbol )
2299 return false;
2300
2301 if( entry.finalName != entry.sanitizedName )
2302 symbol->SetName( entry.finalName );
2303
2304 try
2305 {
2306 if( !symbolLib.FileExists() )
2307 symbolPlugin->SaveLibrary( symbolLib.GetFullPath() );
2308
2309 symbolPlugin->SaveSymbol( symbolLib.GetFullPath(), symbol.get() );
2310 symbol.release();
2311 }
2312 catch( const IO_ERROR& ioe )
2313 {
2314 aError = ioe.What();
2315 return false;
2316 }
2317 }
2318 }
2319
2320 if( aResponseParams )
2321 {
2322 nlohmann::json response = nlohmann::json::object();
2323
2324 if( !skipped.empty() )
2325 response["skipped"] = skipped;
2326
2327 if( !renamedReport.empty() )
2328 response["renamed"] = renamedReport;
2329
2330 *aResponseParams = std::move( response );
2331 }
2332
2333 return true;
2334}
const char * name
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:110
wxString GetSemanticVersion()
Get the semantic version string for KiCad defined inside the KiCadVersion.cmake file in the variable ...
A bitmap button widget that behaves like an AUI toolbar item's button when it is drawn.
int ShowModal() override
virtual void SetParent(EDA_ITEM *aParent)
Definition eda_item.h:113
REMOTE_SYMBOL_CONFIG m_RemoteSymbol
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
Definition kiid.h:49
void ReloadLibraryEntry(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH)
std::optional< LIBRARY_TABLE * > Table(LIBRARY_TABLE_TYPE aType, LIBRARY_TABLE_SCOPE aScope)
Retrieves a given table; creating a new empty project table if a valid project is loaded and the give...
void SetOptions(const wxString &aOptions)
void SetNickname(const wxString &aNickname)
void SetOk(bool aOk=true)
void SetType(const wxString &aType)
void SetDescription(const wxString &aDescription)
void SetURI(const wxString &aUri)
const wxString & URI() const
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:49
int SetLibItemName(const UTF8 &aLibItemName)
Override the library item name portion of the LIB_ID to aLibItemName.
Definition lib_id.cpp:111
int SetLibNickname(const UTF8 &aLibNickname)
Override the logical library name portion of the LIB_ID to aLibNickname.
Definition lib_id.cpp:100
Define a library symbol object.
Definition lib_symbol.h:83
An interface used to output 8 bit text in a convenient way.
Definition richio.h:323
wxString sanitizeFileComponent(const wxString &aComponent, const wxString &aDefault) const
std::optional< wxString > extractUrlFromJson(const wxString &aJsonContent) const
void sendRpcMessage(const wxString &aCommand, nlohmann::json aParameters=nlohmann::json::object(), std::optional< int > aResponseTo=std::nullopt, const wxString &aStatus=wxS("OK"), const std::string &aData=std::string(), const wxString &aErrorCode=wxEmptyString, const wxString &aErrorMessage=wxEmptyString)
bool receiveSymbol(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
void onWebViewLoaded(wxWebViewEvent &aEvent)
void onKicadMessage(const wxString &aMessage)
void handleRemoteLogin(const nlohmann::json &aParams, int aMessageId)
wxString sanitizedPrefix() const
void showMessage(const wxString &aMessage)
wxString jsonString(const nlohmann::json &aObject, const char *aKey) const
bool writeBinaryFile(const wxFileName &aFile, const std::vector< uint8_t > &aData, wxString &aError) const
void handleRpcMessage(const nlohmann::json &aMessage)
std::vector< PCM_INSTALLATION_ENTRY > m_dataSources
bool receiveFootprint(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
bool ensureSymbolLibraryEntry(const wxFileName &aLibraryFile, const wxString &aNickname, bool aGlobalTable, wxString &aError) const
wxString currentDataSourceKey() const
void respondWithError(const wxString &aCommand, int aResponseTo, const wxString &aErrorCode, const wxString &aErrorMessage)
std::optional< wxFileName > findDataSourceJson(const PCM_INSTALLATION_ENTRY &aEntry) const
PANEL_REMOTE_SYMBOL(SCH_EDIT_FRAME *aParent)
bool decompressIfNeeded(const std::string &aCompression, const std::vector< uint8_t > &aInput, std::vector< uint8_t > &aOutput, wxString &aError) const
bool decodeBase64Payload(const std::string &aMessage, std::vector< uint8_t > &aOutPayload, wxString &aError) const
wxString sanitizeForScript(const std::string &aJson) const
std::unique_ptr< LIB_SYMBOL > loadSymbolFromPayload(const std::vector< uint8_t > &aPayload, const wxString &aLibItemName, wxString &aError) const
bool receiveComponent(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError, nlohmann::json *aResponseParams=nullptr)
void onConfigure(wxCommandEvent &aEvent)
bool ensureDestinationRoot(wxFileName &aOutDir, wxString &aError) const
wxString normalizeDataSourceUrl(const wxString &aUrl) const
bool loadDataSource(size_t aIndex)
void onRemoteLoginResult(wxCommandEvent &aEvent)
bool ensureFootprintLibraryEntry(const wxFileName &aLibraryDir, const wxString &aNickname, bool aGlobalTable, wxString &aError) const
bool receiveSPICEModel(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
BITMAP_BUTTON * m_configButton
std::shared_ptr< PLUGIN_CONTENT_MANAGER > m_pcm
bool receive3DModel(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
void storeUserIdForActiveSource(const wxString &aUserId)
BITMAP_BUTTON * m_refreshButton
std::unique_ptr< REMOTE_LOGIN_SERVER > m_loginServer
void onRefresh(wxCommandEvent &aEvent)
void onDataSourceChanged(wxCommandEvent &aEvent)
bool placeDownloadedSymbol(const wxString &aNickname, const wxString &aLibItemName, wxString &aError)
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:129
virtual LIBRARY_MANAGER & GetLibraryManager() const
Definition pgm_base.h:131
Main class of Plugin and Content Manager subsystem.
Definition pcm.h:106
static SYMBOL_LIBRARY_ADAPTER * SymbolLibAdapter(PROJECT *aProject)
Accessor for project symbol library manager adapter.
static TOOL_ACTION placeSymbol
Definition sch_actions.h:66
Schematic editor (Eeschema) main window.
static void SaveSymbol(LIB_SYMBOL *aSymbol, OUTPUTFORMATTER &aFormatter, const wxString &aLibName=wxEmptyString, bool aIncludeData=true)
Base class that schematic file and library loading and saving plugins should derive from.
Definition sch_io.h:59
virtual LIB_SYMBOL * LoadSymbol(const wxString &aLibraryPath, const wxString &aPartName, const std::map< std::string, UTF8 > *aProperties=nullptr)
Load a LIB_SYMBOL object having aPartName from the aLibraryPath containing a library format that this...
Definition sch_io.cpp:100
Schematic symbol object.
Definition sch_symbol.h:76
void AutoplaceFields(SCH_SCREEN *aScreen, AUTOPLACE_ALGO aAlgo) override
Automatically orient all the fields in the symbol.
An interface to the global shared library manager that is schematic-specific and linked to one projec...
std::optional< LIB_STATUS > LoadOne(LIB_DATA *aLib) override
Loads or reloads the given library, if it exists.
LIB_SYMBOL * LoadSymbol(const wxString &aNickname, const wxString &aName)
Load a LIB_SYMBOL having aName from the library given by aNickname.
SAVE_T SaveSymbol(const wxString &aNickname, const LIB_SYMBOL *aSymbol, bool aOverwrite=true)
Write aSymbol to an existing library given by aNickname.
Master controller class:
bool PostAction(const std::string &aActionName, T aParam)
Run the specified action after the current action (coroutine) ends.
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
Definition common.cpp:509
The common library.
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition io_mgr.h:33
PROJECT & Prj()
Definition kicad.cpp:629
LIBRARY_TABLE_SCOPE
STL namespace.
#define REMOTE_SYMBOL_SESSION_VERSION
SETTINGS_MANAGER * GetSettingsManager()
@ PT_DATASOURCE
Definition pcm_data.h:47
PGM_BASE & Pgm()
The global program "get" accessor.
Definition pgm_base.cpp:946
see class PGM_BASE
#define OUTPUTFMTBUFZ
default buffer size for any OUTPUT_FORMATTER
Definition richio.h:305
@ AUTOPLACE_AUTO
Definition sch_item.h:70
T * GetAppSettings(const char *aFilename)
Definition pcm_data.h:158
PCM_PACKAGE package
Definition pcm_data.h:159
wxString identifier
Definition pcm_data.h:118
wxString name
Definition pcm_data.h:115
std::string path
wxString result
Test unit parsing edge cases and error handling.
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:39