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
40
41
42EMBEDDED_FILES::EMBEDDED_FILE* EMBEDDED_FILES::AddFile( const wxFileName& aName, bool aOverwrite )
43{
44 if( HasFile( aName.GetFullName() ) )
45 {
46 if( !aOverwrite )
47 return m_files[aName.GetFullName()];
48
49 m_files.erase( aName.GetFullName() );
50 }
51
52 wxFFileInputStream file( aName.GetFullPath() );
53 wxMemoryBuffer buffer;
54
55 if( !file.IsOk() )
56 return nullptr;
57
58 wxFileOffset length = file.GetLength();
59
60 std::unique_ptr<EMBEDDED_FILE> efile = std::make_unique<EMBEDDED_FILE>();
61 efile->name = aName.GetFullName();
62 efile->decompressedData.resize( length );
63
64 wxString ext = aName.GetExt().Upper();
65
66 // Handle some common file extensions
67 if( ext == "STP" || ext == "STPZ" || ext == "STEP" || ext == "WRL" || ext == "WRZ" )
68 {
70 }
71 else if( ext == "WOFF" || ext == "WOFF2" || ext == "TTF" || ext == "OTF" )
72 {
74 }
75 else if( ext == "PDF" )
76 {
78 }
79 else if( ext == "KICAD_WKS" )
80 {
82 }
83
84 if( !efile->decompressedData.data() )
85 return nullptr;
86
87 char* data = efile->decompressedData.data();
88 wxFileOffset total_read = 0;
89
90 while( !file.Eof() && total_read < length )
91 {
92 file.Read( data, length - total_read );
93
94 size_t read = file.LastRead();
95 data += read;
96 total_read += read;
97 }
98
99 if( CompressAndEncode( *efile ) != RETURN_CODE::OK )
100 return nullptr;
101
102 efile->is_valid = true;
103
104 EMBEDDED_FILE* result = efile.release();
105 m_files[aName.GetFullName()] = result;
106
109
110 return m_files[aName.GetFullName()];
111}
112
113
115{
116 m_files.insert( { aFile->name, aFile } );
117
119 m_fileAddedCallback( aFile );
120}
121
122
123// Remove a file from the collection
124void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
125{
126 auto it = m_files.find( name );
127
128 if( it != m_files.end() )
129 {
130 if( aErase )
131 delete it->second;
132
133 m_files.erase( it );
134 }
135}
136
137
139{
140 for( auto it = m_files.begin(); it != m_files.end(); )
141 {
142 if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
143 {
144 delete it->second;
145 it = m_files.erase( it );
146 }
147 else
148 {
149 ++it;
150 }
151 }
152}
153
154
155// Write the collection of files to a disk file in the specified format
156void EMBEDDED_FILES::WriteEmbeddedFiles( OUTPUTFORMATTER& aOut, bool aWriteData ) const
157{
158 ssize_t MIME_BASE64_LENGTH = 76;
159 aOut.Print( "(embedded_files " );
160
161 for( const auto& [name, entry] : m_files )
162 {
163 const EMBEDDED_FILE& file = *entry;
164
165 // Skip empty files
166 if( file.compressedEncodedData.empty() )
167 {
168 continue;
169 }
170
171 aOut.Print( "(file " );
172 aOut.Print( "(name %s)", aOut.Quotew( file.name ).c_str() );
173
174 const char* type = nullptr;
175
176 switch( file.type )
177 {
178 case EMBEDDED_FILE::FILE_TYPE::DATASHEET: type = "datasheet"; break;
179 case EMBEDDED_FILE::FILE_TYPE::FONT: type = "font"; break;
180 case EMBEDDED_FILE::FILE_TYPE::MODEL: type = "model"; break;
181 case EMBEDDED_FILE::FILE_TYPE::WORKSHEET: type = "worksheet"; break;
182 default: type = "other"; break;
183 }
184
185 aOut.Print( "(type %s)", type );
186
187 if( aWriteData )
188 {
189 aOut.Print( "(data" );
190
191 size_t first = 0;
192
193 while( first < file.compressedEncodedData.length() )
194 {
195 ssize_t remaining = file.compressedEncodedData.length() - first;
196 int length = std::min( remaining, MIME_BASE64_LENGTH );
197
198 std::string_view view( file.compressedEncodedData.data() + first, length );
199
200 aOut.Print( "\n%1s%.*s%s\n", first ? "" : "|", length, view.data(),
201 remaining == length ? "|" : "" );
202 first += MIME_BASE64_LENGTH;
203 }
204
205 aOut.Print( ")" ); // Close data
206 }
207
208 aOut.Print( "(checksum %s)", aOut.Quotew( file.data_hash ).c_str() );
209 aOut.Print( ")" ); // Close file
210 }
211
212 aOut.Print( ")" ); // Close embedded_files
213}
214
215
216// Compress and Base64 encode data
218{
219 std::vector<char> compressedData;
220 size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
221 compressedData.resize( estCompressedSize );
222 size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
223 aFile.decompressedData.data(),
224 aFile.decompressedData.size(), 15 );
225
226 if( ZSTD_isError( compressedSize ) )
227 {
228 compressedData.clear();
230 }
231
232 const size_t dstLen = wxBase64EncodedSize( compressedSize );
233 aFile.compressedEncodedData.resize( dstLen );
234 size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
235 compressedData.data(), compressedSize );
236
237 if( retval != dstLen )
238 {
239 aFile.compressedEncodedData.clear();
241 }
242
244 hash.add( aFile.decompressedData );
245 aFile.data_hash = hash.digest().ToString();
246
247 return RETURN_CODE::OK;
248}
249
250
251// Decompress and Base64 decode data
253{
254 std::vector<char> compressedData;
255 size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
256
257 if( compressedSize == 0 )
258 {
259 wxLogTrace( wxT( "KICAD_EMBED" ),
260 wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
261 __FILE__, __FUNCTION__, __LINE__, aFile.name,
262 aFile.compressedEncodedData.size() );
264 }
265
266 compressedData.resize( compressedSize );
267 void* compressed = compressedData.data();
268
269 // The return value from wxBase64Decode is the actual size of the decoded data avoiding
270 // the modulo 4 padding of the base64 encoding
271 compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
272
273 unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
274
275 if( estDecompressedSize > 1e9 ) // Limit to 1GB
277
278 if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
279 || estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
280 {
282 }
283
284 aFile.decompressedData.resize( estDecompressedSize );
285 void* decompressed = aFile.decompressedData.data();
286
287 size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
288 compressed, compressedSize );
289
290 if( ZSTD_isError( decompressedSize ) )
291 {
292 wxLogTrace( wxT( "KICAD_EMBED" ),
293 wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
294 __FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
295 aFile.decompressedData.clear();
297 }
298
299 aFile.decompressedData.resize( decompressedSize );
300 std::string test_hash;
301 std::string new_hash;
302
304 hash.add( aFile.decompressedData );
305 new_hash = hash.digest().ToString();
306
307 if( aFile.data_hash.length() == 64 )
308 picosha2::hash256_hex_string( aFile.decompressedData, test_hash );
309 else
310 test_hash = new_hash;
311
312 if( test_hash != aFile.data_hash )
313 {
314 wxLogTrace( wxT( "KICAD_EMBED" ),
315 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
316 __FILE__, __FUNCTION__, __LINE__, aFile.name );
317 aFile.decompressedData.clear();
319 }
320
321 aFile.data_hash = new_hash;
322
323 return RETURN_CODE::OK;
324}
325
326
328 std::string& aHash )
329{
330 wxFFileInputStream file( aFileName.GetFullPath() );
331
332 if( !file.IsOk() )
334
335 wxFileOffset length = file.GetLength();
336 std::vector<char> data( length );
337
338 if( !data.data() )
340
341 char* dataPtr = data.data();
342 wxFileOffset totalRead = 0;
343
344 while( !file.Eof() && totalRead < length )
345 {
346 file.Read( dataPtr, length - totalRead );
347 size_t bytesRead = file.LastRead();
348 dataPtr += bytesRead;
349 totalRead += bytesRead;
350 }
351
353 hash.add( data );
354 aHash = hash.digest().ToString();
355
356 return RETURN_CODE::OK;
357}
358
359
360// Parsing method
362{
363 // embedded files are version 20240706 and uses also Bars as separator
364 SetKnowsBar( true );
365
366 if( !aFiles )
367 THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
368 CurLineNumber(), CurOffset() );
369
370 using namespace EMBEDDED_FILES_T;
371
372 std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
373
374 for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
375 {
376 if( token != T_LEFT )
377 Expecting( T_LEFT );
378
379 token = NextTok();
380
381 if( token != T_file )
382 Expecting( "file" );
383
384 if( file )
385 {
386 if( !file->compressedEncodedData.empty() )
387 {
389
390 if( !file->Validate() )
391 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
392 CurLine(), CurLineNumber(), CurOffset() );
393 }
394
395 aFiles->AddFile( file.release() );
396 }
397
398 file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
399
400 for( token = NextTok(); token != T_RIGHT; token = NextTok() )
401 {
402 if( token != T_LEFT )
403 Expecting( T_LEFT );
404
405 token = NextTok();
406
407 switch( token )
408 {
409 case T_checksum:
410 if( !file )
411 Expecting( T_name );
412
413 NeedSYMBOLorNUMBER();
414
415 if( !IsSymbol( token ) )
416 Expecting( "checksum data" );
417
418 file->data_hash = CurStr();
419 NeedRIGHT();
420 break;
421
422 case T_data:
423 if( !file )
424 Expecting( T_name);
425
426 try
427 {
428 NeedBAR();
429 }
430 catch( const PARSE_ERROR& e )
431 {
432 // No data in the file -- due to bug in writer for 9.0.0
433 if( curTok == T_RIGHT )
434 break;
435 else
436 throw e;
437 }
438 catch( ... )
439 {
440 throw;
441 }
442
443 token = NextTok();
444
445 file->compressedEncodedData.reserve( 1 << 17 );
446
447 while( token != T_BAR )
448 {
449 if( !IsSymbol( token ) )
450 Expecting( "base64 file data" );
451
452 file->compressedEncodedData += CurStr();
453 token = NextTok();
454 }
455
456 file->compressedEncodedData.shrink_to_fit();
457
458 NeedRIGHT();
459 break;
460
461 case T_name:
462 if( file )
463 {
464 wxLogTrace( wxT( "KICAD_EMBED" ),
465 wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
466 }
467
468 NeedSYMBOLorNUMBER();
469
470 file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
471 file->name = CurStr();
472 NeedRIGHT();
473
474 break;
475
476 case T_type:
477 if( !file )
478 Expecting( T_name );
479
480 token = NextTok();
481
482 switch( token )
483 {
484 case T_datasheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::DATASHEET; break;
485 case T_font: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::FONT; break;
486 case T_model: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::MODEL; break;
487 case T_worksheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::WORKSHEET; break;
488 case T_other: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::OTHER; break;
489 default: Expecting( "datasheet, font, model, worksheet or other" ); break;
490 }
491
492 NeedRIGHT();
493 break;
494
495 default:
496 Expecting( "checksum, data or name" );
497 }
498 }
499 }
500
501 // Add the last file in the collection
502 if( file )
503 {
504 if( !file->compressedEncodedData.empty() )
505 {
507 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
508 CurLine(), CurLineNumber(), CurOffset() );
509 }
510
511 aFiles->AddFile( file.release() );
512 }
513}
514
515
516wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
517{
518 wxFileName cacheFile;
519
520 auto it = m_files.find( aName );
521
522 if( it == m_files.end() )
523 return cacheFile;
524
525 cacheFile.AssignDir( PATHS::GetUserCachePath() );
526 cacheFile.AppendDir( wxT( "embed" ) );
527
528 if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
529 {
530 wxLogTrace( wxT( "KICAD_EMBED" ),
531 wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
532 __FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
533
534 cacheFile.SetPath( wxFileName::GetTempDir() );
535 }
536
537 wxFileName inputName( aName );
538
539 // Store the cache file name using the data hash to allow for shared data between
540 // multiple projects using the same files as well as deconflicting files with the same name
541 cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
542 cacheFile.SetExt( inputName.GetExt() );
543
544 if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
545 return cacheFile;
546
547 wxFFileOutputStream out( cacheFile.GetFullPath() );
548
549 if( !out.IsOk() )
550 {
551 cacheFile.Clear();
552 return cacheFile;
553 }
554
555 out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
556
557 return cacheFile;
558}
559
560
561const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
562{
563 return &m_fontFiles;
564}
565
566
567const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
568{
569 m_fontFiles.clear();
570
571 for( const auto& [name, entry] : m_files )
572 {
573 if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
574 m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
575 }
576
577 return &m_fontFiles;
578}
579
580
581// Move constructor
583 m_files( std::move( other.m_files ) ),
584 m_fontFiles( std::move( other.m_fontFiles ) ),
585 m_fileAddedCallback( std::move( other.m_fileAddedCallback ) ),
586 m_embedFonts( other.m_embedFonts )
587{
588 other.m_embedFonts = false;
589}
590
591
592// Move assignment operator
594{
595 if (this != &other)
596 {
598 m_files = std::move( other.m_files );
599 m_fontFiles = std::move( other.m_fontFiles );
600 m_fileAddedCallback = std::move( other.m_fileAddedCallback );
601 m_embedFonts = other.m_embedFonts;
602 other.m_embedFonts = false;
603 }
604
605 return *this;
606}
607
608
609// Copy constructor
612{
613 for( const auto& [name, file] : other.m_files )
614 m_files[name] = new EMBEDDED_FILE( *file );
615
616 m_fontFiles = other.m_fontFiles;
618}
619
620
621EMBEDDED_FILES::EMBEDDED_FILES( const EMBEDDED_FILES& other, bool aDeepCopy ) :
623{
624 if( aDeepCopy )
625 {
626 for( const auto& [name, file] : other.m_files )
627 m_files[name] = new EMBEDDED_FILE( *file );
628
629 m_fontFiles = other.m_fontFiles;
630 }
631
633}
634
635
636// Copy assignment operator
638{
639 if( this != &other )
640 {
642
643 for( const auto& [name, file] : other.m_files )
644 m_files[name] = new EMBEDDED_FILE( *file );
645
646 m_fontFiles = other.m_fontFiles;
649 }
650
651 return *this;
652}
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
#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.