KiCad PCB EDA Suite
Loading...
Searching...
No Matches
embedded_files.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, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14 * Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "embedded_files.h"
22
23#include <wx/base64.h>
24#include <wx/debug.h>
25#include <wx/filename.h>
26#include <wx/log.h>
27#include <wx/mstream.h>
28#include <wx/wfstream.h>
29
30#include <map>
31#include <memory>
32#include <sstream>
33
34#include <zstd.h>
35
36#include <kiid.h>
37#include <mmh3_hash.h>
38#include <paths.h>
39#include <picosha2.h>
41
42
44{
46 hash.add( decompressedData );
47
48 is_valid = ( hash.digest().ToString() == data_hash );
49 return is_valid;
50}
51
52
54{
55 std::string new_sha;
56 picosha2::hash256_hex_string( decompressedData, new_sha );
57
58 is_valid = ( new_sha == data_hash );
59 return is_valid;
60}
61
62
64{
65 return wxString::Format( "%s://%s", FILEEXT::KiCadUriPrefix, name );
66}
67
68
69EMBEDDED_FILES::EMBEDDED_FILE* EMBEDDED_FILES::AddFile( const wxFileName& aName, bool aOverwrite )
70{
71 if( HasFile( aName.GetFullName() ) )
72 {
73 if( !aOverwrite )
74 return m_files[aName.GetFullName()];
75
76 m_files.erase( aName.GetFullName() );
77 }
78
79 wxFFileInputStream file( aName.GetFullPath() );
80 wxMemoryBuffer buffer;
81
82 if( !file.IsOk() )
83 return nullptr;
84
85 wxFileOffset length = file.GetLength();
86
87 std::unique_ptr<EMBEDDED_FILE> efile = std::make_unique<EMBEDDED_FILE>();
88 efile->name = aName.GetFullName();
89 efile->decompressedData.resize( length );
90
91 wxString ext = aName.GetExt().Upper();
92
93 // Handle some common file extensions
94 if( ext == "STP" || ext == "STPZ" || ext == "STEP" || ext == "WRL" || ext == "WRZ" )
95 {
97 }
98 else if( ext == "WOFF" || ext == "WOFF2" || ext == "TTF" || ext == "OTF" )
99 {
100 efile->type = EMBEDDED_FILE::FILE_TYPE::FONT;
101 }
102 else if( ext == "PDF" )
103 {
105 }
106 else if( ext == "KICAD_WKS" )
107 {
109 }
110
111 if( !efile->decompressedData.data() )
112 return nullptr;
113
114 char* data = efile->decompressedData.data();
115 wxFileOffset total_read = 0;
116
117 while( !file.Eof() && total_read < length )
118 {
119 file.Read( data, length - total_read );
120
121 size_t read = file.LastRead();
122 data += read;
123 total_read += read;
124 }
125
126 if( CompressAndEncode( *efile ) != RETURN_CODE::OK )
127 return nullptr;
128
129 efile->is_valid = true;
130
131 EMBEDDED_FILE* result = efile.release();
132 m_files[aName.GetFullName()] = result;
133
136
137 return m_files[aName.GetFullName()];
138}
139
140
142{
143 m_files.insert( { aFile->name, aFile } );
144
146 m_fileAddedCallback( aFile );
147}
148
149
150// Remove a file from the collection
151void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
152{
153 auto it = m_files.find( name );
154
155 if( it != m_files.end() )
156 {
157 if( aErase )
158 delete it->second;
159
160 m_files.erase( it );
161 }
162}
163
164
166{
167 for( auto it = m_files.begin(); it != m_files.end(); )
168 {
169 if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
170 {
171 delete it->second;
172 it = m_files.erase( it );
173 }
174 else
175 {
176 ++it;
177 }
178 }
179}
180
181
182// Write the collection of files to a disk file in the specified format
183void EMBEDDED_FILES::WriteEmbeddedFiles( OUTPUTFORMATTER& aOut, bool aWriteData ) const
184{
185 ssize_t MIME_BASE64_LENGTH = 76;
186 aOut.Print( "(embedded_files " );
187
188 for( const auto& [name, entry] : m_files )
189 {
190 const EMBEDDED_FILE& file = *entry;
191
192 // Skip empty files
193 if( file.compressedEncodedData.empty() )
194 {
195 continue;
196 }
197
198 aOut.Print( "(file " );
199 aOut.Print( "(name %s)", aOut.Quotew( file.name ).c_str() );
200
201 const char* type = nullptr;
202
203 switch( file.type )
204 {
205 case EMBEDDED_FILE::FILE_TYPE::DATASHEET: type = "datasheet"; break;
206 case EMBEDDED_FILE::FILE_TYPE::FONT: type = "font"; break;
207 case EMBEDDED_FILE::FILE_TYPE::MODEL: type = "model"; break;
208 case EMBEDDED_FILE::FILE_TYPE::WORKSHEET: type = "worksheet"; break;
209 default: type = "other"; break;
210 }
211
212 aOut.Print( "(type %s)", type );
213
214 if( aWriteData )
215 {
216 aOut.Print( "(data" );
217
218 size_t first = 0;
219
220 while( first < file.compressedEncodedData.length() )
221 {
222 ssize_t remaining = file.compressedEncodedData.length() - first;
223 int length = std::min( remaining, MIME_BASE64_LENGTH );
224
225 std::string_view view( file.compressedEncodedData.data() + first, length );
226
227 aOut.Print( "\n%1s%.*s%s\n", first ? "" : "|", length, view.data(),
228 remaining == length ? "|" : "" );
229 first += MIME_BASE64_LENGTH;
230 }
231
232 aOut.Print( ")" ); // Close data
233 }
234
235 aOut.Print( "(checksum %s)", aOut.Quotew( file.data_hash ).c_str() );
236 aOut.Print( ")" ); // Close file
237 }
238
239 aOut.Print( ")" ); // Close embedded_files
240}
241
242
243// Compress and Base64 encode data
245{
246 std::vector<char> compressedData;
247 size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
248 compressedData.resize( estCompressedSize );
249 size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
250 aFile.decompressedData.data(),
251 aFile.decompressedData.size(), 15 );
252
253 if( ZSTD_isError( compressedSize ) )
254 {
255 compressedData.clear();
257 }
258
259 const size_t dstLen = wxBase64EncodedSize( compressedSize );
260 aFile.compressedEncodedData.resize( dstLen );
261 size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
262 compressedData.data(), compressedSize );
263
264 if( retval != dstLen )
265 {
266 aFile.compressedEncodedData.clear();
268 }
269
271 hash.add( aFile.decompressedData );
272 aFile.data_hash = hash.digest().ToString();
273
274 return RETURN_CODE::OK;
275}
276
277
278// Decompress and Base64 decode data
280{
281 std::vector<char> compressedData;
282 size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
283
284 if( compressedSize == 0 )
285 {
286 wxLogTrace( wxT( "KICAD_EMBED" ),
287 wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
288 __FILE__, __FUNCTION__, __LINE__, aFile.name,
289 aFile.compressedEncodedData.size() );
291 }
292
293 compressedData.resize( compressedSize );
294 void* compressed = compressedData.data();
295
296 // The return value from wxBase64Decode is the actual size of the decoded data avoiding
297 // the modulo 4 padding of the base64 encoding
298 compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
299
300 unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
301
302 if( estDecompressedSize > 1e9 ) // Limit to 1GB
304
305 if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
306 || estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
307 {
309 }
310
311 aFile.decompressedData.resize( estDecompressedSize );
312 void* decompressed = aFile.decompressedData.data();
313
314 size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
315 compressed, compressedSize );
316
317 if( ZSTD_isError( decompressedSize ) )
318 {
319 wxLogTrace( wxT( "KICAD_EMBED" ),
320 wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
321 __FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
322 aFile.decompressedData.clear();
324 }
325
326 aFile.decompressedData.resize( decompressedSize );
327 std::string test_hash;
328 std::string new_hash;
329
331 hash.add( aFile.decompressedData );
332 new_hash = hash.digest().ToString();
333
334 if( aFile.data_hash.length() == 64 )
335 picosha2::hash256_hex_string( aFile.decompressedData, test_hash );
336 else
337 test_hash = new_hash;
338
339 if( test_hash != aFile.data_hash )
340 {
341 wxLogTrace( wxT( "KICAD_EMBED" ),
342 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
343 __FILE__, __FUNCTION__, __LINE__, aFile.name );
344 aFile.decompressedData.clear();
346 }
347
348 aFile.data_hash = new_hash;
349
350 return RETURN_CODE::OK;
351}
352
353
355 std::string& aHash )
356{
357 wxFFileInputStream file( aFileName.GetFullPath() );
358
359 if( !file.IsOk() )
361
362 wxFileOffset length = file.GetLength();
363 std::vector<char> data( length );
364
365 if( !data.data() )
367
368 char* dataPtr = data.data();
369 wxFileOffset totalRead = 0;
370
371 while( !file.Eof() && totalRead < length )
372 {
373 file.Read( dataPtr, length - totalRead );
374 size_t bytesRead = file.LastRead();
375 dataPtr += bytesRead;
376 totalRead += bytesRead;
377 }
378
380 hash.add( data );
381 aHash = hash.digest().ToString();
382
383 return RETURN_CODE::OK;
384}
385
386
387// Parsing method
389{
390 // embedded files are version 20240706 and uses also Bars as separator
391 SetKnowsBar( true );
392
393 if( !aFiles )
394 THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
395 CurLineNumber(), CurOffset() );
396
397 using namespace EMBEDDED_FILES_T;
398
399 std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
400
401 for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
402 {
403 if( token != T_LEFT )
404 Expecting( T_LEFT );
405
406 token = NextTok();
407
408 if( token != T_file )
409 Expecting( "file" );
410
411 if( file )
412 {
413 if( !file->compressedEncodedData.empty() )
414 {
416
417 if( !file->Validate() )
418 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
419 CurLine(), CurLineNumber(), CurOffset() );
420 }
421
422 aFiles->AddFile( file.release() );
423 }
424
425 file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
426
427 for( token = NextTok(); token != T_RIGHT; token = NextTok() )
428 {
429 if( token != T_LEFT )
430 Expecting( T_LEFT );
431
432 token = NextTok();
433
434 switch( token )
435 {
436 case T_checksum:
437 if( !file )
438 Expecting( T_name );
439
440 NeedSYMBOLorNUMBER();
441
442 if( !IsSymbol( token ) )
443 Expecting( "checksum data" );
444
445 file->data_hash = CurStr();
446 NeedRIGHT();
447 break;
448
449 case T_data:
450 if( !file )
451 Expecting( T_name);
452
453 try
454 {
455 NeedBAR();
456 }
457 catch( const PARSE_ERROR& e )
458 {
459 // No data in the file -- due to bug in writer for 9.0.0
460 if( curTok == T_RIGHT )
461 break;
462 else
463 throw e;
464 }
465 catch( ... )
466 {
467 throw;
468 }
469
470 token = NextTok();
471
472 file->compressedEncodedData.reserve( 1 << 17 );
473
474 while( token != T_BAR )
475 {
476 if( !IsSymbol( token ) )
477 Expecting( "base64 file data" );
478
479 file->compressedEncodedData += CurStr();
480 token = NextTok();
481 }
482
483 file->compressedEncodedData.shrink_to_fit();
484
485 NeedRIGHT();
486 break;
487
488 case T_name:
489 if( file )
490 {
491 wxLogTrace( wxT( "KICAD_EMBED" ),
492 wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
493 }
494
495 NeedSYMBOLorNUMBER();
496
497 file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
498 file->name = CurStr();
499 NeedRIGHT();
500
501 break;
502
503 case T_type:
504 if( !file )
505 Expecting( T_name );
506
507 token = NextTok();
508
509 switch( token )
510 {
511 case T_datasheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::DATASHEET; break;
512 case T_font: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::FONT; break;
513 case T_model: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::MODEL; break;
514 case T_worksheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::WORKSHEET; break;
515 case T_other: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::OTHER; break;
516 default: Expecting( "datasheet, font, model, worksheet or other" ); break;
517 }
518
519 NeedRIGHT();
520 break;
521
522 default:
523 Expecting( "checksum, data or name" );
524 }
525 }
526 }
527
528 // Add the last file in the collection
529 if( file )
530 {
531 if( !file->compressedEncodedData.empty() )
532 {
534 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
535 CurLine(), CurLineNumber(), CurOffset() );
536 }
537
538 aFiles->AddFile( file.release() );
539 }
540}
541
542
543wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
544{
545 wxFileName cacheFile;
546
547 auto it = m_files.find( aName );
548
549 if( it == m_files.end() )
550 return cacheFile;
551
552 cacheFile.AssignDir( PATHS::GetUserCachePath() );
553 cacheFile.AppendDir( wxT( "embed" ) );
554
555 if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
556 {
557 wxLogTrace( wxT( "KICAD_EMBED" ),
558 wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
559 __FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
560
561 cacheFile.SetPath( wxFileName::GetTempDir() );
562 }
563
564 wxFileName inputName( aName );
565
566 // Store the cache file name using the data hash to allow for shared data between
567 // multiple projects using the same files as well as deconflicting files with the same name
568 cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
569 cacheFile.SetExt( inputName.GetExt() );
570
571 if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
572 return cacheFile;
573
574 wxFFileOutputStream out( cacheFile.GetFullPath() );
575
576 if( !out.IsOk() )
577 {
578 cacheFile.Clear();
579 return cacheFile;
580 }
581
582 out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
583
584 return cacheFile;
585}
586
587
588const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
589{
590 return &m_fontFiles;
591}
592
593
594const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
595{
596 m_fontFiles.clear();
597
598 for( const auto& [name, entry] : m_files )
599 {
600 if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
601 m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
602 }
603
604 return &m_fontFiles;
605}
606
607
608// Move constructor
610 m_files( std::move( other.m_files ) ),
611 m_fontFiles( std::move( other.m_fontFiles ) ),
612 m_fileAddedCallback( std::move( other.m_fileAddedCallback ) ),
613 m_embedFonts( other.m_embedFonts )
614{
615 other.m_embedFonts = false;
616}
617
618
619// Move assignment operator
621{
622 if (this != &other)
623 {
625 m_files = std::move( other.m_files );
626 m_fontFiles = std::move( other.m_fontFiles );
627 m_fileAddedCallback = std::move( other.m_fileAddedCallback );
628 m_embedFonts = other.m_embedFonts;
629 other.m_embedFonts = false;
630 }
631
632 return *this;
633}
634
635
636// Copy constructor
639{
640 for( const auto& [name, file] : other.m_files )
641 m_files[name] = new EMBEDDED_FILE( *file );
642
643 m_fontFiles = other.m_fontFiles;
645}
646
647
648EMBEDDED_FILES::EMBEDDED_FILES( const EMBEDDED_FILES& other, bool aDeepCopy ) :
650{
651 if( aDeepCopy )
652 {
653 for( const auto& [name, file] : other.m_files )
654 m_files[name] = new EMBEDDED_FILE( *file );
655
656 m_fontFiles = other.m_fontFiles;
657 }
658
660}
661
662
663// Copy assignment operator
665{
666 if( this != &other )
667 {
669
670 for( const auto& [name, file] : other.m_files )
671 m_files[name] = new EMBEDDED_FILE( *file );
672
673 m_fontFiles = other.m_fontFiles;
676 }
677
678 return *this;
679}
const char * name
void ParseEmbedded(EMBEDDED_FILES *aFiles)
std::vector< wxString > m_fontFiles
void RemoveFile(const wxString &name, bool aErase=true)
Remove a file from the collection and frees the memory.
@ OUT_OF_MEMORY
Could not allocate memory.
@ FILE_NOT_FOUND
File not found on disk.
@ CHECKSUM_ERROR
Checksum in file does not match data.
wxFileName GetTemporaryFileName(const wxString &aName) const
void WriteEmbeddedFiles(OUTPUTFORMATTER &aOut, bool aWriteData) const
Output formatter for the embedded files.
const std::vector< wxString > * UpdateFontFiles()
Helper function to get a list of fonts for fontconfig to add to the library.
FILE_ADDED_CALLBACK m_fileAddedCallback
static RETURN_CODE DecompressAndDecode(EMBEDDED_FILE &aFile)
Takes data from the #compressedEncodedData buffer and Base64 decodes it.
static RETURN_CODE ComputeFileHash(const wxFileName &aFileName, std::string &aHash)
Compute the hash of a file on disk without fully embedding it.
bool HasFile(const wxString &name) const
void ClearEmbeddedFiles(bool aDeleteFiles=true)
void ClearEmbeddedFonts()
Remove all embedded fonts from the collection.
EMBEDDED_FILES & operator=(EMBEDDED_FILES &&other) noexcept
EMBEDDED_FILE * AddFile(const wxFileName &aName, bool aOverwrite)
Load a file from disk and adds it to the collection.
static uint32_t Seed()
const std::vector< wxString > * GetFontFiles() const
If we just need the cached version of the font files, we can use this function which is const and wil...
EMBEDDED_FILES()=default
static RETURN_CODE CompressAndEncode(EMBEDDED_FILE &aFile)
Take data from the #decompressedData buffer and compresses it using ZSTD into the #compressedEncodedD...
std::map< wxString, EMBEDDED_FILE * > m_files
bool m_embedFonts
If set, fonts will be embedded in the element on save.
A streaming C++ equivalent for MurmurHash3_x64_128.
Definition mmh3_hash.h:60
FORCE_INLINE void add(const std::string &input)
Definition mmh3_hash.h:95
FORCE_INLINE HASH_128 digest()
Definition mmh3_hash.h:114
An interface used to output 8 bit text in a convenient way.
Definition richio.h:295
std::string Quotew(const wxString &aWrapee) const
Definition richio.cpp:507
int PRINTF_FUNC_N Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition richio.cpp:422
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition paths.cpp:508
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition paths.cpp:450
static const std::string KiCadUriPrefix
#define THROW_PARSE_ERROR(aProblem, aSource, aInputLine, aLineNumber, aByteIndex)
#define MIME_BASE64_LENGTH
std::vector< char > decompressedData
std::string ToString() const
Definition hash_128.h:47
A filename or source description, a problem input line, a line number, a byte offset,...
wxString result
Test unit parsing edge cases and error handling.
Definition of file extensions used in Kicad.