KiCad PCB EDA Suite
Loading...
Searching...
No Matches
database_lib_settings.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 (C) 2022 Jon Evans <[email protected]>
5* Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
6*
7* This program is free software; you can redistribute it and/or
8* modify it under the terms of the GNU General Public License
9* as published by the Free Software Foundation; either version 2
10* of the License, or (at your option) any later version.
11*
12* This program is distributed in the hope that it will be useful,
13* but WITHOUT ANY WARRANTY; without even the implied warranty of
14* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15* GNU General Public License for more details.
16*
17* You should have received a copy of the GNU General Public License
18* along with this program. If not, see <https://www.gnu.org/licenses/>.
19*/
20
21#include <core/kicad_algo.h>
22#include <json_common.h>
23#include <algorithm>
24#include <set>
25
27#include <kiplatform/secrets.h>
29#include <settings/parameters.h>
30#include <string_utils.h>
32
33#include <wx/filename.h>
34
35
36const int dblibSchemaVersion = 1;
37
38
40 visible_on_add( false ),
41 visible_in_chooser( false ),
42 show_name( false ),
43 inherit_properties( false ),
44 source( nlohmann::json::object() )
45{
46}
47
48
49DATABASE_FIELD_MAPPING::DATABASE_FIELD_MAPPING( const std::string& aColumn, const std::string& aName,
50 bool aVisibleOnAdd, bool aVisibleInChooser,
51 bool aShowName, bool aInheritProperties ) :
52 column( aColumn ),
53 name( aName ),
54 name_wx( aName.c_str(), wxConvUTF8 ),
55 visible_on_add( aVisibleOnAdd ),
56 visible_in_chooser( aVisibleInChooser ),
57 show_name( aShowName ),
58 inherit_properties( aInheritProperties ),
59 source( nlohmann::json::object() )
60{
61}
62
63
64void DATABASE_FIELD_MAPPING::SetName( const std::string& aName )
65{
66 name = aName;
67 name_wx = wxString( aName.c_str(), wxConvUTF8 );
68}
69
70
75template <typename T>
76static void patchKey( nlohmann::json& aObj, const std::string& aKey, const T& aValue, const T& aParsed,
77 bool aRequired )
78{
79 if( aValue == aParsed && ( aObj.contains( aKey ) || !aRequired ) )
80 return;
81
82 if constexpr( std::is_same_v<T, std::string> )
83 {
84 if( aValue.empty() && !aRequired )
85 {
86 aObj.erase( aKey );
87 return;
88 }
89 }
90
91 aObj[aKey] = aValue;
92}
93
94
96{
97 DATABASE_FIELD_MAPPING field( fetchOrDefault<std::string>( aEntry, "column" ),
98 fetchOrDefault<std::string>( aEntry, "name" ),
99 fetchOrDefault<bool>( aEntry, "visible_on_add" ),
100 fetchOrDefault<bool>( aEntry, "visible_in_chooser" ),
101 fetchOrDefault<bool>( aEntry, "show_name" ),
102 fetchOrDefault<bool>( aEntry, "inherit_properties" ) );
103
104 field.source = aEntry;
105 return field;
106}
107
108
110{
111 nlohmann::json out = aField.source.is_object() ? aField.source : nlohmann::json::object();
113
114 patchKey( out, "column", aField.column, parsed.column, true );
115 patchKey( out, "name", aField.name, parsed.name, true );
116 patchKey( out, "visible_on_add", aField.visible_on_add, parsed.visible_on_add, false );
117 patchKey( out, "visible_in_chooser", aField.visible_in_chooser, parsed.visible_in_chooser, false );
118 patchKey( out, "show_name", aField.show_name, parsed.show_name, false );
119 patchKey( out, "inherit_properties", aField.inherit_properties, parsed.inherit_properties, false );
120
121 return out;
122}
123
124
126{
128
129 table.source = aEntry;
130 table.name = fetchOrDefault<std::string>( aEntry, "name" );
131 table.table = fetchOrDefault<std::string>( aEntry, "table" );
132 table.key_col = fetchOrDefault<std::string>( aEntry, "key" );
133 table.symbols_col = fetchOrDefault<std::string>( aEntry, "symbols" );
134 table.footprints_col = fetchOrDefault<std::string>( aEntry, "footprints" );
135 table.pins_col = fetchOrDefault<std::string>( aEntry, "pins" );
136
137 // Sanitize library display names; currently only `/` is removed because we
138 // use it as a separator and allow it in symbol names.
139 std::erase( table.name, '/' );
140
141 if( aEntry.contains( "properties" ) && aEntry["properties"].is_object() )
142 {
143 const nlohmann::json& propJson = aEntry["properties"];
144
145 table.properties.description = fetchOrDefault<std::string>( propJson, "description" );
146 table.properties.footprint_filters = fetchOrDefault<std::string>( propJson, "footprint_filters" );
147 table.properties.keywords = fetchOrDefault<std::string>( propJson, "keywords" );
148 table.properties.exclude_from_bom = fetchOrDefault<std::string>( propJson, "exclude_from_bom" );
149 table.properties.exclude_from_board = fetchOrDefault<std::string>( propJson, "exclude_from_board" );
150 table.properties.exclude_from_sim = fetchOrDefault<std::string>( propJson, "exclude_from_sim" );
151 }
152
153 if( aEntry.contains( "fields" ) && aEntry["fields"].is_array() )
154 {
155 for( const nlohmann::json& fieldJson : aEntry["fields"] )
156 {
157 if( fieldJson.empty() || !fieldJson.is_object() )
158 continue;
159
160 table.fields.emplace_back( fieldFromJson( fieldJson ) );
161 }
162 }
163
164 return table;
165}
166
167
169{
170 nlohmann::json out = aTable.source.is_object() ? aTable.source : nlohmann::json::object();
171 DATABASE_LIB_TABLE parsed = tableFromJson( out );
172
173 patchKey( out, "name", aTable.name, parsed.name, true );
174 patchKey( out, "table", aTable.table, parsed.table, true );
175 patchKey( out, "key", aTable.key_col, parsed.key_col, true );
176 patchKey( out, "symbols", aTable.symbols_col, parsed.symbols_col, true );
177 patchKey( out, "footprints", aTable.footprints_col, parsed.footprints_col, true );
178 patchKey( out, "pins", aTable.pins_col, parsed.pins_col, false );
179
180 const MAPPABLE_SYMBOL_PROPERTIES& props = aTable.properties;
181 const MAPPABLE_SYMBOL_PROPERTIES& oldProps = parsed.properties;
182 bool hadProps = out.contains( "properties" );
183 nlohmann::json propJson = hadProps ? out["properties"] : nlohmann::json::object();
184
185 // A wrong-typed raw value is left alone unless the user changed a mapping
186 if( props != oldProps && !propJson.is_object() )
187 propJson = nlohmann::json::object();
188
189 if( propJson.is_object() )
190 {
191 patchKey( propJson, "description", props.description, oldProps.description, false );
192 patchKey( propJson, "footprint_filters", props.footprint_filters, oldProps.footprint_filters, false );
193 patchKey( propJson, "keywords", props.keywords, oldProps.keywords, false );
194 patchKey( propJson, "exclude_from_bom", props.exclude_from_bom, oldProps.exclude_from_bom, false );
195 patchKey( propJson, "exclude_from_board", props.exclude_from_board, oldProps.exclude_from_board, false );
196 patchKey( propJson, "exclude_from_sim", props.exclude_from_sim, oldProps.exclude_from_sim, false );
197
198 if( hadProps || !propJson.empty() )
199 out["properties"] = propJson;
200 }
201
202 if( !aTable.fields.empty() || out.contains( "fields" ) )
203 {
204 nlohmann::json fieldsJson = nlohmann::json::array();
205
206 for( const DATABASE_FIELD_MAPPING& field : aTable.fields )
207 fieldsJson.push_back( fieldToJson( field ) );
208
209 // Keep raw entries the parser skipped (non-objects) when the mapping was not edited
210 bool fieldsEdited = aTable.fields.size() != parsed.fields.size();
211
212 for( size_t i = 0; !fieldsEdited && i < aTable.fields.size(); ++i )
213 fieldsEdited = fieldsJson[i] != fieldToJson( parsed.fields[i] );
214
215 if( fieldsEdited || !out.contains( "fields" ) )
216 out["fields"] = fieldsJson;
217 }
218
219 return out;
220}
221
222
223DATABASE_LIB_SETTINGS::DATABASE_LIB_SETTINGS( const std::string& aFilename ) :
225{
226 m_params.emplace_back( new PARAM<std::string>( "name", &m_Name, "" ) );
227
228 m_params.emplace_back( new PARAM<std::string>( "description", &m_Description, "" ) );
229
230 m_params.emplace_back( new PARAM<std::string>( "source.dsn", &m_Source.dsn, "" ) );
231
232 m_params.emplace_back( new PARAM<std::string>( "source.username", &m_Source.username, "" ) );
233
234 m_params.emplace_back( new PARAM<std::string>( "source.password", &m_Source.password, "" ) );
235
236 m_params.emplace_back( new PARAM<std::string>( "source.connection_string",
237 &m_Source.connection_string, "" ) );
238
239 m_params.emplace_back( new PARAM<int>( "source.timeout_seconds", &m_Source.timeout, 2 ) );
240
241 m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>(
242 "libraries",
243 [&]() -> nlohmann::json
244 {
245 nlohmann::json libraries = nlohmann::json::array();
246
247 for( const DATABASE_LIB_TABLE& table : m_Tables )
248 libraries.push_back( tableToJson( table ) );
249
250 return libraries;
251 },
252 [&]( const nlohmann::json& aObj )
253 {
254 m_Tables.clear();
255
256 if( !aObj.is_array() )
257 return;
258
259 for( const nlohmann::json& entry : aObj )
260 {
261 if( entry.empty() || !entry.is_object() )
262 continue;
263
264 m_Tables.emplace_back( tableFromJson( entry ) );
265 }
266 },
267 {} ) );
268
269 m_params.emplace_back( new PARAM<int>( "cache.max_size", &m_Cache.max_size, 256 ) );
270
271 m_params.emplace_back( new PARAM<int>( "cache.max_age", &m_Cache.max_age, 60 ) );
272
273 m_params.emplace_back( new PARAM<bool>( "globally_unique_keys", &m_GloballyUniqueKeys, false ) );
274
275 registerMigration( 0, 1,
276 [&]() -> bool
277 {
278 /*
279 * Schema 0 -> 1
280 * Move internal symbol properties from fields with special names to
281 * a separate place in the schema.
282 */
283 if( !Contains( "libraries" ) || !At( "libraries" ).is_array() )
284 return true;
285
286 for( nlohmann::json& library : At( "libraries" ) )
287 {
288 if( !library.contains( "fields" ) )
289 continue;
290
291 for( const nlohmann::json& field : library["fields"] )
292 {
293 if( !field.contains( "name" ) || !field.contains( "column" ) )
294 continue;
295
296 std::string name = field["name"].get<std::string>();
297 std::string col = field["column"].get<std::string>();
298
299 if( name == "ki_description" )
300 library["properties"]["description"] = col;
301 else if( name == "ki_fp_filters" )
302 library["properties"]["footprint_filters"] = col;
303 }
304 }
305
306 return true;
307 } );
308}
309
310
312static constexpr const char* c_scalarParents[] = { "/source", "/cache" };
313
314
317{
318 for( const char* parent : c_scalarParents )
319 {
320 nlohmann::json::json_pointer ptr( parent );
321
322 if( aInternals.contains( ptr ) && !aInternals[ptr].is_object() )
323 aInternals[ptr] = nlohmann::json::object();
324 }
325}
326
327
329{
331
332 // A schema migration saves right after loading, and that save already needs this baseline
334}
335
336
338{
340
341 bool modified = JSON_SETTINGS::Store();
342
343 const nlohmann::json& original = originalJson();
344
345 // A new file gets every key written
346 if( !original.is_object() || original.empty() )
347 return modified;
348
349 const std::vector<std::pair<std::string, bool>> unchanged = {
350 { "name", m_Name == m_loaded.name },
351 { "description", m_Description == m_loaded.description },
352 { "source.dsn", m_Source.dsn == m_loaded.source.dsn },
353 { "source.username", m_Source.username == m_loaded.source.username },
354 { "source.password", m_Source.password == m_loaded.source.password },
355 { "source.connection_string", m_Source.connection_string == m_loaded.source.connection_string },
356 { "source.timeout_seconds", m_Source.timeout == m_loaded.source.timeout },
357 { "cache.max_size", m_Cache.max_size == m_loaded.cache.max_size },
358 { "cache.max_age", m_Cache.max_age == m_loaded.cache.max_age },
359 { "globally_unique_keys", m_GloballyUniqueKeys == m_loaded.globallyUniqueKeys }
360 };
361
362 std::set<std::string> editedParents;
363
364 for( const auto& [path, isUnchanged] : unchanged )
365 {
366 nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( path );
367
368 if( !isUnchanged )
369 {
370 editedParents.insert( ptr.parent_pointer().to_string() );
371 continue;
372 }
373
374 if( original.contains( ptr ) )
375 ( *Internals() )[ptr] = original[ptr];
376 else if( Internals()->contains( ptr.parent_pointer() ) )
377 ( *Internals() )[ptr.parent_pointer()].erase( ptr.back() );
378 }
379
380 // Base Store() creates parent objects, so restore wholly untouched ones to their raw form
381 for( const char* parent : c_scalarParents )
382 {
383 nlohmann::json::json_pointer ptr( parent );
384
385 if( editedParents.contains( parent ) )
386 continue;
387
388 if( original.contains( ptr ) )
389 ( *Internals() )[ptr] = original[ptr];
390 else
391 Internals()->erase( ptr.back() );
392 }
393
394 return modified;
395}
396
397
399{
401 m_loaded.source.password = m_Source.password;
402}
403
404
409
410
411std::unique_ptr<DATABASE_LIB_SETTINGS> LoadDatabaseLibSettingsReadOnly( const wxString& aPath, bool* aLoaded )
412{
413 auto settings = std::make_unique<DATABASE_LIB_SETTINGS>( std::string( aPath.ToUTF8() ) );
414
415 // Read-only before loading so a schema migration cannot rewrite the file
416 settings->SetReadOnly( true );
417 bool loaded = settings->LoadFromFile();
418
419 // Mirror the path LoadFromFile() reads, which forces the extension; an unreadable file loads as
420 // defaults with success reported
421 wxFileName path( aPath );
423
424 if( aLoaded )
425 *aLoaded = !path.FileExists() || ( loaded && path.IsFileReadable() );
426
427 return settings;
428}
429
430
431wxString DefaultDatabaseLibFileName( const wxString& aLibName )
432{
433 wxString name = aLibName;
434 name.Trim( true ).Trim( false );
436
437 // A name of only dots would hide the file or name its folder
438 if( name.IsEmpty() || name.find_first_not_of( wxS( "._" ) ) == wxString::npos )
439 return wxS( "database_library" );
440
441 return name;
442}
443
444
445static wxString passwordService( const DATABASE_SOURCE& aSource )
446{
447 return wxS( "KiCad ODBC:" ) + wxString::FromUTF8( aSource.dsn );
448}
449
450
455
456
458{
459 using NO_PROMPT_FN = bool ( * )( const wxString&, const wxString&, wxString& );
460
461 const NO_PROMPT_FN* target = aLookup.target<NO_PROMPT_FN>();
462
463 return target && *target == &KIPLATFORM::SECRETS::GetSecretNoPrompt;
464}
465
466
467static bool hasKeyringLogin( const DATABASE_SOURCE& aSource )
468{
469 // Without a user name the DSN supplies its own credentials, so the keyring is not consulted
470 return aSource.connection_string.empty() && !aSource.dsn.empty() && !aSource.username.empty();
471}
472
473
474bool GetDatabasePassword( const DATABASE_SOURCE& aSource, std::string& aPassword,
475 const DATABASE_SECRET_LOOKUP& aLookup )
476{
477 if( !hasKeyringLogin( aSource ) )
478 return false;
479
480 DATABASE_SECRET_LOOKUP lookup = aLookup ? aLookup : DefaultDatabaseSecretLookup();
481 wxString secret;
482
483 if( !lookup( passwordService( aSource ), wxString::FromUTF8( aSource.username ), secret ) )
484 return false;
485
486 aPassword = secret.utf8_string();
487 return true;
488}
489
490
492{
493 if( aSource.password.empty() )
494 GetDatabasePassword( aSource, aSource.password, aLookup );
495}
496
497
498bool StoreDatabasePassword( const DATABASE_SOURCE& aSource, const std::string& aPassword )
499{
500 if( !hasKeyringLogin( aSource ) )
501 return false;
502
503 wxString service = passwordService( aSource );
504 wxString user = wxString::FromUTF8( aSource.username );
505
506 if( aPassword.empty() )
507 return KIPLATFORM::SECRETS::DeleteSecret( service, user );
508
509 return KIPLATFORM::SECRETS::StoreSecret( service, user, wxString::FromUTF8( aPassword ) );
510}
const char * name
static DATABASE_LIB_TABLE tableFromJson(const nlohmann::json &aEntry)
Missing or mistyped keys load as defaults so one incomplete entry cannot stop the others.
void ResolvePassword(const DATABASE_SECRET_LOOKUP &aLookup=nullptr)
Fills in the source password from the keyring when the file has none.
DATABASE_LIB_SETTINGS(const std::string &aFilename)
wxString getFileExt() const override
static DATABASE_FIELD_MAPPING fieldFromJson(const nlohmann::json &aEntry)
struct DATABASE_LIB_SETTINGS::LOADED_SCALARS m_loaded
bool Store() override
Stores parameters, then restores the raw file value of every scalar the caller left unchanged so that...
void Load() override
Records the scalars as loaded; runs before a schema migration writes the file back.
static nlohmann::json tableToJson(const DATABASE_LIB_TABLE &aTable)
DATABASE_CACHE_SETTINGS m_Cache
static nlohmann::json fieldToJson(const DATABASE_FIELD_MAPPING &aField)
static nlohmann::json::json_pointer PointerFromString(std::string aPath)
Builds a JSON pointer based on a given string.
const nlohmann::json & originalJson() const
virtual void Load()
Updates the parameters of this object based on the current JSON document contents.
std::vector< PARAM_BASE * > m_params
The list of parameters (owned by this object)
JSON_SETTINGS_INTERNALS * Internals()
static ResultType fetchOrDefault(const nlohmann::json &aJson, const std::string &aKey, ResultType aDefault=ResultType())
Helper to retrieve a value from a JSON object (dictionary) as a certain result type.
JSON_SETTINGS(const wxString &aFilename, SETTINGS_LOC aLocation, int aSchemaVersion)
virtual bool Store()
Stores the current parameters into the JSON document represented by this object Note: this doesn't do...
Like a normal param, but with custom getter and setter functions.
Definition parameters.h:299
wxString DefaultDatabaseLibFileName(const wxString &aLibName)
const int dblibSchemaVersion
bool IsNoPromptSecretLookup(const DATABASE_SECRET_LOOKUP &aLookup)
static constexpr const char * c_scalarParents[]
Objects that hold scalar settings rather than library tables.
static void normalizeScalarParents(JSON_SETTINGS_INTERNALS &aInternals)
Nested params cannot store into a non-object parent, so replace a malformed one before storing.
static void patchKey(nlohmann::json &aObj, const std::string &aKey, const T &aValue, const T &aParsed, bool aRequired)
Writes aValue to aObj[aKey] only if it differs from aParsed, the value the raw key was read as.
std::unique_ptr< DATABASE_LIB_SETTINGS > LoadDatabaseLibSettingsReadOnly(const wxString &aPath, bool *aLoaded)
Loads a database library settings file without allowing the load itself (e.g.
static wxString passwordService(const DATABASE_SOURCE &aSource)
void ResolveDatabasePassword(DATABASE_SOURCE &aSource, const DATABASE_SECRET_LOOKUP &aLookup)
Fills in the source password from the keyring, but only when the library file did not supply one.
static bool hasKeyringLogin(const DATABASE_SOURCE &aSource)
bool StoreDatabasePassword(const DATABASE_SOURCE &aSource, const std::string &aPassword)
Stores the password for the source's DSN login in the system keyring, or removes the entry when the p...
DATABASE_SECRET_LOOKUP DefaultDatabaseSecretLookup()
The lookup used when none is injected, which must never prompt to unlock the keyring.
bool GetDatabasePassword(const DATABASE_SOURCE &aSource, std::string &aPassword, const DATABASE_SECRET_LOOKUP &aLookup)
DSN passwords that are not saved in the (often shared) library file live in the system keyring,...
KICOMMON_API void ResolveDatabasePassword(DATABASE_SOURCE &aSource, const DATABASE_SECRET_LOOKUP &aLookup=nullptr)
Fills in the source password from the keyring, but only when the library file did not supply one.
std::function< bool(const wxString &aService, const wxString &aKey, wxString &aSecret)> DATABASE_SECRET_LOOKUP
Keyring lookup signature, matching KIPLATFORM::SECRETS::GetSecretNoPrompt.
@ NONE
Definition eda_fill.h:42
nlohmann::json json
Definition gerbview.cpp:50
static const std::string DatabaseLibraryFileExtension
SETTINGS_LOC
bool DeleteSecret(const wxString &aService, const wxString &aKey)
bool StoreSecret(const wxString &aService, const wxString &aKey, const wxString &aSecret)
bool GetSecretNoPrompt(const wxString &aService, const wxString &aKey, wxString &aSecret)
Like GetSecret(), but never waits on the user.
bool ReplaceIllegalFileNameChars(std::string &aName, int aReplaceChar)
Checks aName for illegal file name characters.
bool visible_in_chooser
Whether the column is shown by default in the chooser.
std::string column
Database column name.
std::string name
KiCad field name.
void SetName(const std::string &aName)
bool inherit_properties
Whether or not to inherit properties from symbol field.
bool visible_on_add
Whether to show the field when placing the symbol.
bool show_name
Whether or not to show the field name as well as its value.
nlohmann::json source
Raw JSON as loaded; untouched keys are written back verbatim.
wxString name_wx
KiCad field name (converted)
A database library table will be mapped to a sub-library provided by the database library entry in th...
std::string key_col
Unique key column name (will form part of the LIB_ID)
std::string name
KiCad library nickname (will form part of the LIB_ID)
nlohmann::json source
Raw JSON as loaded; untouched keys are written back verbatim.
std::string symbols_col
Column name containing KiCad symbol refs.
std::string footprints_col
Column name containing KiCad footprint refs.
std::vector< DATABASE_FIELD_MAPPING > fields
std::string table
Database table to pull content from.
MAPPABLE_SYMBOL_PROPERTIES properties
std::string pins_col
Column name containing JSON pin assignments (optional)
std::string connection_string
std::string path
Definition of file extensions used in Kicad.