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