Files
Seth Hillbrand c52ab91c84 Reduce compile times by extracting headers
Break the most expensive header include chains that contaminate the
largest number of translation units

New lightweight headers for extractdded types:
font_metrics.h, mouse_drag_action.h, board_project_settings_params.h,
common_settings_internals.h, wx_infobar_message_type.h,
zone_layer_properties_grid.h
2026-02-13 17:10:45 -08:00

680 lines
19 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The 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 <http://www.gnu.org/licenses/>.
*/
#include "embedded_files.h"
#include "embedded_files_parser.h"
#include <wx/base64.h>
#include <wx/debug.h>
#include <wx/filename.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include <wx/wfstream.h>
#include <map>
#include <memory>
#include <sstream>
#include <zstd.h>
#include <kiid.h>
#include <mmh3_hash.h>
#include <paths.h>
#include <picosha2.h>
#include <wildcards_and_files_ext.h>
bool EMBEDDED_FILES::EMBEDDED_FILE::Validate()
{
MMH3_HASH hash( EMBEDDED_FILES::Seed() );
hash.add( decompressedData );
is_valid = ( hash.digest().ToString() == data_hash );
return is_valid;
}
bool EMBEDDED_FILES::EMBEDDED_FILE::Validate_SHA256()
{
std::string new_sha;
picosha2::hash256_hex_string( decompressedData, new_sha );
is_valid = ( new_sha == data_hash );
return is_valid;
}
wxString EMBEDDED_FILES::EMBEDDED_FILE::GetLink() const
{
return wxString::Format( "%s://%s", FILEEXT::KiCadUriPrefix, name );
}
EMBEDDED_FILES::EMBEDDED_FILE* EMBEDDED_FILES::AddFile( const wxFileName& aName, bool aOverwrite )
{
if( HasFile( aName.GetFullName() ) )
{
if( !aOverwrite )
return m_files[aName.GetFullName()];
m_files.erase( aName.GetFullName() );
}
wxFFileInputStream file( aName.GetFullPath() );
wxMemoryBuffer buffer;
if( !file.IsOk() )
return nullptr;
wxFileOffset length = file.GetLength();
std::unique_ptr<EMBEDDED_FILE> efile = std::make_unique<EMBEDDED_FILE>();
efile->name = aName.GetFullName();
efile->decompressedData.resize( length );
wxString ext = aName.GetExt().Upper();
// Handle some common file extensions
if( ext == "STP" || ext == "STPZ" || ext == "STEP" || ext == "WRL" || ext == "WRZ" )
{
efile->type = EMBEDDED_FILE::FILE_TYPE::MODEL;
}
else if( ext == "WOFF" || ext == "WOFF2" || ext == "TTF" || ext == "OTF" )
{
efile->type = EMBEDDED_FILE::FILE_TYPE::FONT;
}
else if( ext == "PDF" )
{
efile->type = EMBEDDED_FILE::FILE_TYPE::DATASHEET;
}
else if( ext == "KICAD_WKS" )
{
efile->type = EMBEDDED_FILE::FILE_TYPE::WORKSHEET;
}
if( !efile->decompressedData.data() )
return nullptr;
char* data = efile->decompressedData.data();
wxFileOffset total_read = 0;
while( !file.Eof() && total_read < length )
{
file.Read( data, length - total_read );
size_t read = file.LastRead();
data += read;
total_read += read;
}
if( CompressAndEncode( *efile ) != RETURN_CODE::OK )
return nullptr;
efile->is_valid = true;
EMBEDDED_FILE* result = efile.release();
m_files[aName.GetFullName()] = result;
if( m_fileAddedCallback )
m_fileAddedCallback( result );
return m_files[aName.GetFullName()];
}
void EMBEDDED_FILES::AddFile( EMBEDDED_FILE* aFile )
{
m_files.insert( { aFile->name, aFile } );
if( m_fileAddedCallback )
m_fileAddedCallback( aFile );
}
// Remove a file from the collection
void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
{
auto it = m_files.find( name );
if( it != m_files.end() )
{
if( aErase )
delete it->second;
m_files.erase( it );
}
}
void EMBEDDED_FILES::ClearEmbeddedFonts()
{
for( auto it = m_files.begin(); it != m_files.end(); )
{
if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
{
delete it->second;
it = m_files.erase( it );
}
else
{
++it;
}
}
}
// Write the collection of files to a disk file in the specified format
void EMBEDDED_FILES::WriteEmbeddedFiles( OUTPUTFORMATTER& aOut, bool aWriteData ) const
{
ssize_t MIME_BASE64_LENGTH = 76;
aOut.Print( "(embedded_files " );
for( const auto& [name, entry] : m_files )
{
const EMBEDDED_FILE& file = *entry;
// Skip empty files
if( file.compressedEncodedData.empty() )
{
continue;
}
aOut.Print( "(file " );
aOut.Print( "(name %s)", aOut.Quotew( file.name ).c_str() );
const char* type = nullptr;
switch( file.type )
{
case EMBEDDED_FILE::FILE_TYPE::DATASHEET: type = "datasheet"; break;
case EMBEDDED_FILE::FILE_TYPE::FONT: type = "font"; break;
case EMBEDDED_FILE::FILE_TYPE::MODEL: type = "model"; break;
case EMBEDDED_FILE::FILE_TYPE::WORKSHEET: type = "worksheet"; break;
default: type = "other"; break;
}
aOut.Print( "(type %s)", type );
if( aWriteData )
{
aOut.Print( "(data" );
size_t first = 0;
while( first < file.compressedEncodedData.length() )
{
ssize_t remaining = file.compressedEncodedData.length() - first;
int length = std::min( remaining, MIME_BASE64_LENGTH );
std::string_view view( file.compressedEncodedData.data() + first, length );
aOut.Print( "\n%1s%.*s%s\n", first ? "" : "|", length, view.data(),
remaining == length ? "|" : "" );
first += MIME_BASE64_LENGTH;
}
aOut.Print( ")" ); // Close data
}
aOut.Print( "(checksum %s)", aOut.Quotew( file.data_hash ).c_str() );
aOut.Print( ")" ); // Close file
}
aOut.Print( ")" ); // Close embedded_files
}
// Compress and Base64 encode data
EMBEDDED_FILES::RETURN_CODE EMBEDDED_FILES::CompressAndEncode( EMBEDDED_FILE& aFile )
{
std::vector<char> compressedData;
size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
compressedData.resize( estCompressedSize );
size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
aFile.decompressedData.data(),
aFile.decompressedData.size(), 15 );
if( ZSTD_isError( compressedSize ) )
{
compressedData.clear();
return RETURN_CODE::OUT_OF_MEMORY;
}
const size_t dstLen = wxBase64EncodedSize( compressedSize );
aFile.compressedEncodedData.resize( dstLen );
size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
compressedData.data(), compressedSize );
if( retval != dstLen )
{
aFile.compressedEncodedData.clear();
return RETURN_CODE::OUT_OF_MEMORY;
}
MMH3_HASH hash( EMBEDDED_FILES::Seed() );
hash.add( aFile.decompressedData );
aFile.data_hash = hash.digest().ToString();
return RETURN_CODE::OK;
}
// Decompress and Base64 decode data
EMBEDDED_FILES::RETURN_CODE EMBEDDED_FILES::DecompressAndDecode( EMBEDDED_FILE& aFile )
{
std::vector<char> compressedData;
size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
if( compressedSize == 0 )
{
wxLogTrace( wxT( "KICAD_EMBED" ),
wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
__FILE__, __FUNCTION__, __LINE__, aFile.name,
aFile.compressedEncodedData.size() );
return RETURN_CODE::OUT_OF_MEMORY;
}
compressedData.resize( compressedSize );
void* compressed = compressedData.data();
// The return value from wxBase64Decode is the actual size of the decoded data avoiding
// the modulo 4 padding of the base64 encoding
compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
if( estDecompressedSize > 1e9 ) // Limit to 1GB
return RETURN_CODE::OUT_OF_MEMORY;
if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
|| estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
{
return RETURN_CODE::OUT_OF_MEMORY;
}
aFile.decompressedData.resize( estDecompressedSize );
void* decompressed = aFile.decompressedData.data();
size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
compressed, compressedSize );
if( ZSTD_isError( decompressedSize ) )
{
wxLogTrace( wxT( "KICAD_EMBED" ),
wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
__FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
aFile.decompressedData.clear();
return RETURN_CODE::OUT_OF_MEMORY;
}
aFile.decompressedData.resize( decompressedSize );
std::string test_hash;
std::string new_hash;
MMH3_HASH hash( EMBEDDED_FILES::Seed() );
hash.add( aFile.decompressedData );
new_hash = hash.digest().ToString();
if( aFile.data_hash.length() == 64 )
picosha2::hash256_hex_string( aFile.decompressedData, test_hash );
else
test_hash = new_hash;
if( test_hash != aFile.data_hash )
{
wxLogTrace( wxT( "KICAD_EMBED" ),
wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
__FILE__, __FUNCTION__, __LINE__, aFile.name );
aFile.decompressedData.clear();
return RETURN_CODE::CHECKSUM_ERROR;
}
aFile.data_hash = new_hash;
return RETURN_CODE::OK;
}
EMBEDDED_FILES::RETURN_CODE EMBEDDED_FILES::ComputeFileHash( const wxFileName& aFileName,
std::string& aHash )
{
wxFFileInputStream file( aFileName.GetFullPath() );
if( !file.IsOk() )
return RETURN_CODE::FILE_NOT_FOUND;
wxFileOffset length = file.GetLength();
std::vector<char> data( length );
if( !data.data() )
return RETURN_CODE::OUT_OF_MEMORY;
char* dataPtr = data.data();
wxFileOffset totalRead = 0;
while( !file.Eof() && totalRead < length )
{
file.Read( dataPtr, length - totalRead );
size_t bytesRead = file.LastRead();
dataPtr += bytesRead;
totalRead += bytesRead;
}
MMH3_HASH hash( EMBEDDED_FILES::Seed() );
hash.add( data );
aHash = hash.digest().ToString();
return RETURN_CODE::OK;
}
// Parsing method
void EMBEDDED_FILES_PARSER::ParseEmbedded( EMBEDDED_FILES* aFiles )
{
// embedded files are version 20240706 and uses also Bars as separator
SetKnowsBar( true );
if( !aFiles )
THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
CurLineNumber(), CurOffset() );
using namespace EMBEDDED_FILES_T;
std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token != T_LEFT )
Expecting( T_LEFT );
token = NextTok();
if( token != T_file )
Expecting( "file" );
if( file )
{
if( !file->compressedEncodedData.empty() )
{
EMBEDDED_FILES::DecompressAndDecode( *file );
if( !file->Validate() )
THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
CurLine(), CurLineNumber(), CurOffset() );
}
aFiles->AddFile( file.release() );
}
file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
for( token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token != T_LEFT )
Expecting( T_LEFT );
token = NextTok();
switch( token )
{
case T_checksum:
if( !file )
Expecting( T_name );
NeedSYMBOLorNUMBER();
if( !IsSymbol( token ) )
Expecting( "checksum data" );
file->data_hash = CurStr();
NeedRIGHT();
break;
case T_data:
if( !file )
Expecting( T_name);
try
{
NeedBAR();
}
catch( const PARSE_ERROR& e )
{
// No data in the file -- due to bug in writer for 9.0.0
if( curTok == T_RIGHT )
break;
else
throw e;
}
catch( ... )
{
throw;
}
token = NextTok();
file->compressedEncodedData.reserve( 1 << 17 );
while( token != T_BAR )
{
if( !IsSymbol( token ) )
Expecting( "base64 file data" );
file->compressedEncodedData += CurStr();
token = NextTok();
}
file->compressedEncodedData.shrink_to_fit();
NeedRIGHT();
break;
case T_name:
if( file )
{
wxLogTrace( wxT( "KICAD_EMBED" ),
wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
}
NeedSYMBOLorNUMBER();
file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
file->name = CurStr();
NeedRIGHT();
break;
case T_type:
if( !file )
Expecting( T_name );
token = NextTok();
switch( token )
{
case T_datasheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::DATASHEET; break;
case T_font: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::FONT; break;
case T_model: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::MODEL; break;
case T_worksheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::WORKSHEET; break;
case T_other: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::OTHER; break;
default: Expecting( "datasheet, font, model, worksheet or other" ); break;
}
NeedRIGHT();
break;
default:
Expecting( "checksum, data or name" );
}
}
}
// Add the last file in the collection
if( file )
{
if( !file->compressedEncodedData.empty() )
{
if( EMBEDDED_FILES::DecompressAndDecode( *file ) == EMBEDDED_FILES::RETURN_CODE::CHECKSUM_ERROR )
THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
CurLine(), CurLineNumber(), CurOffset() );
}
aFiles->AddFile( file.release() );
}
}
wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
{
wxFileName cacheFile;
auto it = m_files.find( aName );
if( it == m_files.end() )
return cacheFile;
cacheFile.AssignDir( PATHS::GetUserCachePath() );
cacheFile.AppendDir( wxT( "embed" ) );
if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
{
wxLogTrace( wxT( "KICAD_EMBED" ),
wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
__FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
cacheFile.SetPath( wxFileName::GetTempDir() );
}
wxFileName inputName( aName );
// Store the cache file name using the data hash to allow for shared data between
// multiple projects using the same files as well as deconflicting files with the same name
cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
cacheFile.SetExt( inputName.GetExt() );
if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
return cacheFile;
wxFFileOutputStream out( cacheFile.GetFullPath() );
if( !out.IsOk() )
{
cacheFile.Clear();
return cacheFile;
}
out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
return cacheFile;
}
const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
{
return &m_fontFiles;
}
const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
{
m_fontFiles.clear();
for( const auto& [name, entry] : m_files )
{
if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
}
return &m_fontFiles;
}
// Move constructor
EMBEDDED_FILES::EMBEDDED_FILES( EMBEDDED_FILES&& other ) noexcept :
m_files( std::move( other.m_files ) ),
m_fontFiles( std::move( other.m_fontFiles ) ),
m_fileAddedCallback( std::move( other.m_fileAddedCallback ) ),
m_embedFonts( other.m_embedFonts )
{
other.m_embedFonts = false;
}
// Move assignment operator
EMBEDDED_FILES& EMBEDDED_FILES::operator=(EMBEDDED_FILES&& other) noexcept
{
if (this != &other)
{
ClearEmbeddedFiles();
m_files = std::move( other.m_files );
m_fontFiles = std::move( other.m_fontFiles );
m_fileAddedCallback = std::move( other.m_fileAddedCallback );
m_embedFonts = other.m_embedFonts;
other.m_embedFonts = false;
}
return *this;
}
// Copy constructor
EMBEDDED_FILES::EMBEDDED_FILES( const EMBEDDED_FILES& other ) :
m_embedFonts( other.m_embedFonts )
{
for( const auto& [name, file] : other.m_files )
m_files[name] = new EMBEDDED_FILE( *file );
m_fontFiles = other.m_fontFiles;
m_fileAddedCallback = other.m_fileAddedCallback;
}
EMBEDDED_FILES::EMBEDDED_FILES( const EMBEDDED_FILES& other, bool aDeepCopy ) :
m_embedFonts( other.m_embedFonts )
{
if( aDeepCopy )
{
for( const auto& [name, file] : other.m_files )
m_files[name] = new EMBEDDED_FILE( *file );
m_fontFiles = other.m_fontFiles;
}
m_fileAddedCallback = other.m_fileAddedCallback;
}
// Copy assignment operator
EMBEDDED_FILES& EMBEDDED_FILES::operator=( const EMBEDDED_FILES& other )
{
if( this != &other )
{
ClearEmbeddedFiles();
for( const auto& [name, file] : other.m_files )
m_files[name] = new EMBEDDED_FILE( *file );
m_fontFiles = other.m_fontFiles;
m_fileAddedCallback = other.m_fileAddedCallback;
m_embedFonts = other.m_embedFonts;
}
return *this;
}