/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2020 Jon Evans * Copyright (C) 2020-2023 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include nlohmann::json::json_pointer JSON_SETTINGS_INTERNALS::PointerFromString( std::string aPath ) { std::replace( aPath.begin(), aPath.end(), '.', '/' ); aPath.insert( 0, "/" ); nlohmann::json::json_pointer p; try { p = nlohmann::json::json_pointer( aPath ); } catch( ... ) { wxASSERT_MSG( false, wxT( "Invalid pointer path in PointerFromString!" ) ); } return p; } JSON_SETTINGS::JSON_SETTINGS( const wxString& aFilename, SETTINGS_LOC aLocation, int aSchemaVersion, bool aCreateIfMissing, bool aCreateIfDefault, bool aWriteFile ) : m_filename( aFilename ), m_legacy_filename( "" ), m_location( aLocation ), m_createIfMissing( aCreateIfMissing ), m_createIfDefault( aCreateIfDefault ), m_writeFile( aWriteFile ), m_deleteLegacyAfterMigration( true ), m_resetParamsIfMissing( true ), m_schemaVersion( aSchemaVersion ), m_manager( nullptr ) { m_internals = std::make_unique(); try { m_internals->SetFromString( "meta.filename", GetFullFilename() ); } catch( ... ) { wxLogTrace( traceSettings, wxT( "Error: Could not create filename field for %s" ), GetFullFilename() ); } m_params.emplace_back( new PARAM( "meta.version", &m_schemaVersion, m_schemaVersion, true ) ); } JSON_SETTINGS::~JSON_SETTINGS() { for( PARAM_BASE* param: m_params ) delete param; m_params.clear(); } wxString JSON_SETTINGS::GetFullFilename() const { if( m_filename.BeforeLast( '.' ) == getFileExt() ) return m_filename; return wxString( m_filename + "." + getFileExt() ); } nlohmann::json& JSON_SETTINGS::At( const std::string& aPath ) { return m_internals->At( aPath ); } bool JSON_SETTINGS::Contains( const std::string& aPath ) const { return m_internals->contains( JSON_SETTINGS_INTERNALS::PointerFromString( aPath ) ); } JSON_SETTINGS_INTERNALS* JSON_SETTINGS::Internals() { return m_internals.get(); } void JSON_SETTINGS::Load() { for( PARAM_BASE* param : m_params ) { try { param->Load( this, m_resetParamsIfMissing ); } catch( ... ) { // Skip unreadable parameters in file wxLogTrace( traceSettings, wxT( "param '%s' load err" ), param->GetJsonPath().c_str() ); } } } bool JSON_SETTINGS::LoadFromFile( const wxString& aDirectory ) { // First, load all params to default values m_internals->clear(); Load(); bool success = true; bool migrated = false; bool legacy_migrated = false; LOCALE_IO locale; auto migrateFromLegacy = [&] ( wxFileName& aPath ) { // Backup and restore during migration so that the original can be mutated if // convenient bool backed_up = false; wxFileName temp; if( aPath.IsDirWritable() ) { temp.AssignTempFileName( aPath.GetFullPath() ); if( !wxCopyFile( aPath.GetFullPath(), temp.GetFullPath() ) ) { wxLogTrace( traceSettings, wxT( "%s: could not create temp file for migration" ), GetFullFilename() ); } else { backed_up = true; } } // Silence popups if legacy file is read-only wxLogNull doNotLog; wxConfigBase::DontCreateOnDemand(); auto cfg = std::make_unique( wxT( "" ), wxT( "" ), aPath.GetFullPath() ); // If migrate fails or is not implemented, fall back to built-in defaults that // were already loaded above if( !MigrateFromLegacy( cfg.get() ) ) { success = false; wxLogTrace( traceSettings, wxT( "%s: migrated; not all settings were found in legacy file" ), GetFullFilename() ); } else { success = true; wxLogTrace( traceSettings, wxT( "%s: migrated from legacy format" ), GetFullFilename() ); } if( backed_up ) { cfg.reset(); if( !wxCopyFile( temp.GetFullPath(), aPath.GetFullPath() ) ) { wxLogTrace( traceSettings, wxT( "migrate; copy temp file %s to %s failed" ), temp.GetFullPath(), aPath.GetFullPath() ); } if( !wxRemoveFile( temp.GetFullPath() ) ) { wxLogTrace( traceSettings, wxT( "migrate; failed to remove temp file %s" ), temp.GetFullPath() ); } } // Either way, we want to clean up the old file afterwards legacy_migrated = true; }; wxFileName path; if( aDirectory.empty() ) { path.Assign( m_filename ); path.SetExt( getFileExt() ); } else { wxString dir( aDirectory ); path.Assign( dir, m_filename, getFileExt() ); } if( !path.Exists() ) { // Case 1: legacy migration, no .json extension yet path.SetExt( getLegacyFileExt() ); if( path.Exists() ) { migrateFromLegacy( path ); } // Case 2: legacy filename is different from new one else if( !m_legacy_filename.empty() ) { path.SetName( m_legacy_filename ); if( path.Exists() ) migrateFromLegacy( path ); } else { success = false; } } else { if( !path.IsFileWritable() ) m_writeFile = false; try { wxFFileInputStream fp( path.GetFullPath(), wxT( "rt" ) ); wxStdInputStream fstream( fp ); if( fp.IsOk() ) { *static_cast( m_internals.get() ) = nlohmann::json::parse( fstream, nullptr, /* allow_exceptions = */ true, /* ignore_comments = */ true ); // Save whatever we loaded, before doing any migration etc m_internals->m_original = *static_cast( m_internals.get() ); // If parse succeeds, check if schema migration is required int filever = -1; try { filever = m_internals->Get( "meta.version" ); } catch( ... ) { wxLogTrace( traceSettings, wxT( "%s: file version could not be read!" ), GetFullFilename() ); success = false; } if( filever >= 0 && filever < m_schemaVersion ) { wxLogTrace( traceSettings, wxT( "%s: attempting migration from version " "%d to %d" ), GetFullFilename(), filever, m_schemaVersion ); if( Migrate() ) { migrated = true; } else { wxLogTrace( traceSettings, wxT( "%s: migration failed!" ), GetFullFilename() ); } } else if( filever > m_schemaVersion ) { wxLogTrace( traceSettings, wxT( "%s: warning: file version %d is newer than latest (%d)" ), GetFullFilename(), filever, m_schemaVersion ); } } else { wxLogTrace( traceSettings, wxT( "%s exists but can't be opened for read" ), GetFullFilename() ); } } catch( nlohmann::json::parse_error& error ) { success = false; wxLogTrace( traceSettings, wxT( "Json parse error reading %s: %s" ), path.GetFullPath(), error.what() ); wxLogTrace( traceSettings, wxT( "Attempting migration in case file is in legacy " "format" ) ); migrateFromLegacy( path ); } } // Now that we have new data in the JSON structure, load the params again Load(); // And finally load any nested settings for( NESTED_SETTINGS* settings : m_nested_settings ) settings->LoadFromFile(); wxLogTrace( traceSettings, wxT( "Loaded <%s> with schema %d" ), GetFullFilename(), m_schemaVersion ); // If we migrated, clean up the legacy file (with no extension) if( m_writeFile && ( legacy_migrated || migrated ) ) { if( legacy_migrated && m_deleteLegacyAfterMigration && !wxRemoveFile( path.GetFullPath() ) ) { wxLogTrace( traceSettings, wxT( "Warning: could not remove legacy file %s" ), path.GetFullPath() ); } // And write-out immediately so that we don't lose data if the program later crashes. if( m_deleteLegacyAfterMigration ) SaveToFile( aDirectory, true ); } return success; } bool JSON_SETTINGS::Store() { bool modified = false; for( PARAM_BASE* param : m_params ) { modified |= !param->MatchesFile( this ); param->Store( this ); } return modified; } void JSON_SETTINGS::ResetToDefaults() { for( PARAM_BASE* param : m_params ) param->SetDefault(); } bool JSON_SETTINGS::SaveToFile( const wxString& aDirectory, bool aForce ) { if( !m_writeFile ) return false; // Default PROJECT won't have a filename set if( m_filename.IsEmpty() ) return false; wxFileName path; if( aDirectory.empty() ) { path.Assign( m_filename ); path.SetExt( getFileExt() ); } else { wxString dir( aDirectory ); path.Assign( dir, m_filename, getFileExt() ); } if( !m_createIfMissing && !path.FileExists() ) { wxLogTrace( traceSettings, wxT( "File for %s doesn't exist and m_createIfMissing == false; not saving" ), GetFullFilename() ); return false; } // Ensure the path exists, and create it if not. if( !path.DirExists() && !path.Mkdir() ) { wxLogTrace( traceSettings, wxT( "Warning: could not create path %s, can't save %s" ), path.GetPath(), GetFullFilename() ); return false; } if( ( path.FileExists() && !path.IsFileWritable() ) || ( !path.FileExists() && !path.IsDirWritable() ) ) { wxLogTrace( traceSettings, wxT( "File for %s is read-only; not saving" ), GetFullFilename() ); return false; } bool modified = false; for( NESTED_SETTINGS* settings : m_nested_settings ) modified |= settings->SaveToFile(); modified |= Store(); if( !modified && !aForce && path.FileExists() ) { wxLogTrace( traceSettings, wxT( "%s contents not modified, skipping save" ), GetFullFilename() ); return false; } else if( !modified && !aForce && !m_createIfDefault ) { wxLogTrace( traceSettings, wxT( "%s contents still default and m_createIfDefault == false; not saving" ), GetFullFilename() ); return false; } wxLogTrace( traceSettings, wxT( "Saving %s" ), GetFullFilename() ); LOCALE_IO dummy; bool success = true; nlohmann::json toSave = m_internals->m_original; for( PARAM_BASE* param : m_params ) { if( PARAM_WXSTRING_MAP* stringMap = dynamic_cast( param ) ) { if( stringMap->ClearUnknownKeys() ) toSave[ stringMap->GetJsonPath() ] = nlohmann::json( {} ); } } toSave.update( m_internals->begin(), m_internals->end(), /* merge_objects = */ true ); try { std::stringstream buffer; buffer << std::setw( 2 ) << toSave << std::endl; wxFFileOutputStream fileStream( path.GetFullPath(), "wb" ); if( !fileStream.IsOk() || !fileStream.WriteAll( buffer.str().c_str(), buffer.str().size() ) ) { wxLogTrace( traceSettings, wxT( "Warning: could not save %s" ), GetFullFilename() ); success = false; } } catch( nlohmann::json::exception& error ) { wxLogTrace( traceSettings, wxT( "Catch error: could not save %s. Json error %s" ), GetFullFilename(), error.what() ); success = false; } catch( ... ) { wxLogTrace( traceSettings, wxT( "Error: could not save %s." ) ); success = false; } return success; } const std::string JSON_SETTINGS::FormatAsString() { Store(); LOCALE_IO dummy; std::stringstream buffer; buffer << std::setw( 2 ) << *m_internals << std::endl; return buffer.str(); } bool JSON_SETTINGS::LoadFromRawFile( const wxString& aPath ) { try { wxFFileInputStream fp( aPath, wxT( "rt" ) ); wxStdInputStream fstream( fp ); if( fp.IsOk() ) { *static_cast( m_internals.get() ) = nlohmann::json::parse( fstream, nullptr, /* allow_exceptions = */ true, /* ignore_comments = */ true ); } else { return false; } } catch( nlohmann::json::parse_error& error ) { wxLogTrace( traceSettings, wxT( "Json parse error reading %s: %s" ), aPath, error.what() ); return false; } // Now that we have new data in the JSON structure, load the params again Load(); return true; } std::optional JSON_SETTINGS::GetJson( const std::string& aPath ) const { nlohmann::json::json_pointer ptr = m_internals->PointerFromString( aPath ); if( m_internals->contains( ptr ) ) { try { return std::optional{ m_internals->at( ptr ) }; } catch( ... ) { } } return std::optional{}; } template std::optional JSON_SETTINGS::Get( const std::string& aPath ) const { if( std::optional ret = GetJson( aPath ) ) { try { return ret->get(); } catch( ... ) { } } return std::nullopt; } // Instantiate all required templates here to allow reducing scope of json.hpp template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template KICOMMON_API std::optional JSON_SETTINGS::Get( const std::string& aPath ) const; template void JSON_SETTINGS::Set( const std::string& aPath, ValueType aVal ) { m_internals->SetFromString( aPath, std::move( aVal ) ); } // Instantiate all required templates here to allow reducing scope of json.hpp template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, bool aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, double aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, float aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, int aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, unsigned int aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, unsigned long long aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, const char* aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, std::string aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, nlohmann::json aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, KIGFX::COLOR4D aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, BOM_FIELD aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, BOM_PRESET aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, BOM_FMT_PRESET aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, GRID aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, wxPoint aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, wxSize aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, wxRect aValue ); template KICOMMON_API void JSON_SETTINGS::Set( const std::string& aPath, wxAuiPaneInfo aValue ); void JSON_SETTINGS::registerMigration( int aOldSchemaVersion, int aNewSchemaVersion, std::function aMigrator ) { wxASSERT( aNewSchemaVersion > aOldSchemaVersion ); wxASSERT( aNewSchemaVersion <= m_schemaVersion ); m_migrators[aOldSchemaVersion] = std::make_pair( aNewSchemaVersion, aMigrator ); } bool JSON_SETTINGS::Migrate() { int filever = m_internals->Get( "meta.version" ); while( filever < m_schemaVersion ) { wxASSERT( m_migrators.count( filever ) > 0 ); if( !m_migrators.count( filever ) ) { wxLogTrace( traceSettings, wxT( "Migrator missing for %s version %d!" ), typeid( *this ).name(), filever ); return false; } std::pair> pair = m_migrators.at( filever ); if( pair.second() ) { wxLogTrace( traceSettings, wxT( "Migrated %s from %d to %d" ), typeid( *this ).name(), filever, pair.first ); filever = pair.first; m_internals->At( "meta.version" ) = filever; } else { wxLogTrace( traceSettings, wxT( "Migration failed for %s from %d to %d" ), typeid( *this ).name(), filever, pair.first ); return false; } } return true; } bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig ) { wxLogTrace( traceSettings, wxT( "MigrateFromLegacy() not implemented for %s" ), typeid( *this ).name() ); return false; } bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, wxString& aTarget ) { nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath ); if( aObj.contains( ptr ) && aObj.at( ptr ).is_string() ) { aTarget = aObj.at( ptr ).get(); return true; } return false; } bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, bool& aTarget ) { nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath ); if( aObj.contains( ptr ) && aObj.at( ptr ).is_boolean() ) { aTarget = aObj.at( ptr ).get(); return true; } return false; } bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, int& aTarget ) { nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath ); if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_integer() ) { aTarget = aObj.at( ptr ).get(); return true; } return false; } bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, unsigned int& aTarget ) { nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath ); if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_unsigned() ) { aTarget = aObj.at( ptr ).get(); return true; } return false; } template bool JSON_SETTINGS::fromLegacy( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest ) { ValueType val; if( aConfig->Read( aKey, &val ) ) { try { ( *m_internals )[aDest] = val; } catch( ... ) { wxASSERT_MSG( false, wxT( "Could not write value in fromLegacy!" ) ); return false; } return true; } return false; } // Explicitly declare these because we only support a few types anyway, and it means we can keep // wxConfig detail out of the header file template KICOMMON_API bool JSON_SETTINGS::fromLegacy( wxConfigBase*, const std::string&, const std::string& ); template KICOMMON_API bool JSON_SETTINGS::fromLegacy( wxConfigBase*, const std::string&, const std::string& ); template KICOMMON_API bool JSON_SETTINGS::fromLegacy( wxConfigBase*, const std::string&, const std::string& ); bool JSON_SETTINGS::fromLegacyString( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest ) { wxString str; if( aConfig->Read( aKey, &str ) ) { try { ( *m_internals )[aDest] = str.ToUTF8(); } catch( ... ) { wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyString!" ) ); return false; } return true; } return false; } bool JSON_SETTINGS::fromLegacyColor( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest ) { wxString str; if( aConfig->Read( aKey, &str ) ) { KIGFX::COLOR4D color; color.SetFromWxString( str ); try { nlohmann::json js = nlohmann::json::array( { color.r, color.g, color.b, color.a } ); ( *m_internals )[aDest] = std::move( js ); } catch( ... ) { wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyColor!" ) ); return false; } return true; } return false; } void JSON_SETTINGS::AddNestedSettings( NESTED_SETTINGS* aSettings ) { wxLogTrace( traceSettings, wxT( "AddNestedSettings %s" ), aSettings->GetFilename() ); m_nested_settings.push_back( aSettings ); } void JSON_SETTINGS::ReleaseNestedSettings( NESTED_SETTINGS* aSettings ) { if( !aSettings || !m_manager ) return; auto it = std::find_if( m_nested_settings.begin(), m_nested_settings.end(), [&aSettings]( const JSON_SETTINGS* aPtr ) { return aPtr == aSettings; } ); if( it != m_nested_settings.end() ) { wxLogTrace( traceSettings, wxT( "Flush and release %s" ), ( *it )->GetFilename() ); ( *it )->SaveToFile(); m_nested_settings.erase( it ); } aSettings->SetParent( nullptr ); } // Specializations to allow conversion between wxString and std::string via JSON_SETTINGS API template<> std::optional JSON_SETTINGS::Get( const std::string& aPath ) const { if( std::optional opt_json = GetJson( aPath ) ) return wxString( opt_json->get().c_str(), wxConvUTF8 ); return std::nullopt; } template<> void JSON_SETTINGS::Set( const std::string& aPath, wxString aVal ) { ( *m_internals )[aPath] = aVal.ToUTF8(); } template ResultType JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey, ResultType aDefault ) { ResultType ret = std::move( aDefault ); try { if( aJson.contains( aKey ) ) ret = aJson.at( aKey ).get(); } catch( ... ) { } return ret; } template KICOMMON_API std::string JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey, std::string aDefault ); template KICOMMON_API bool JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey, bool aDefault );