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 (C) 2024 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
14 * General 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/file.h>
26#include <wx/filename.h>
27#include <wx/log.h>
28#include <wx/mstream.h>
29#include <wx/wfstream.h>
30
31#include <map>
32#include <memory>
33#include <sstream>
34
35#include <zstd.h>
36
37#include <kiid.h>
38#include <mmh3_hash.h>
39#include <paths.h>
40
41
42
43EMBEDDED_FILES::EMBEDDED_FILE* EMBEDDED_FILES::AddFile( const wxFileName& aName, bool aOverwrite )
44{
45 if( HasFile( aName.GetFullName() ) )
46 {
47 if( !aOverwrite )
48 return m_files[aName.GetFullName()];
49
50 m_files.erase( aName.GetFullName() );
51 }
52
53 wxFFileInputStream file( aName.GetFullPath() );
54 wxMemoryBuffer buffer;
55
56 if( !file.IsOk() )
57 return nullptr;
58
59 wxFileOffset length = file.GetLength();
60
61 std::unique_ptr<EMBEDDED_FILE> efile = std::make_unique<EMBEDDED_FILE>();
62 efile->name = aName.GetFullName();
63 efile->decompressedData.resize( length );
64
65 wxString ext = aName.GetExt().Upper();
66
67 // Handle some common file extensions
68 if( ext == "STP" || ext == "STPZ" || ext == "STEP" || ext == "WRL" || ext == "WRZ" )
69 {
71 }
72 else if( ext == "WOFF" || ext == "WOFF2" || ext == "TTF" || ext == "OTF" )
73 {
75 }
76 else if( ext == "PDF" )
77 {
79 }
80 else if( ext == "KICAD_WKS" )
81 {
83 }
84
85 if( !efile->decompressedData.data() )
86 return nullptr;
87
88 char* data = efile->decompressedData.data();
89 wxFileOffset total_read = 0;
90
91 while( !file.Eof() && total_read < length )
92 {
93 file.Read( data, length - total_read );
94
95 size_t read = file.LastRead();
96 data += read;
97 total_read += read;
98 }
99
100 if( CompressAndEncode( *efile ) != RETURN_CODE::OK )
101 return nullptr;
102
103 efile->is_valid = true;
104
105 m_files[aName.GetFullName()] = efile.release();
106
107 return m_files[aName.GetFullName()];
108}
109
110
112{
113 m_files.insert( { aFile->name, aFile } );
114}
115
116// Remove a file from the collection
117void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
118{
119 auto it = m_files.find( name );
120
121 if( it != m_files.end() )
122 {
123 m_files.erase( it );
124
125 if( aErase )
126 delete it->second;
127 }
128}
129
130
132{
133 for( auto it = m_files.begin(); it != m_files.end(); )
134 {
135 if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
136 {
137 delete it->second;
138 it = m_files.erase( it );
139 }
140 else
141 {
142 ++it;
143 }
144 }
145}
146
147
148// Write the collection of files to a disk file in the specified format
150 bool aWriteData ) const
151{
152 ssize_t MIME_BASE64_LENGTH = 76;
153 aOut.Print( aNestLevel, "(embedded_files\n" );
154
155 for( const auto& [name, entry] : m_files )
156 {
157 const EMBEDDED_FILE& file = *entry;
158
159 aOut.Print( aNestLevel + 1, "(file\n" );
160 aOut.Print( aNestLevel + 2, "(name \"%s\")\n", file.name.c_str().AsChar() );
161
162 const char* type = nullptr;
163
164 switch( file.type )
165 {
167 type = "datasheet";
168 break;
170 type = "font";
171 break;
173 type = "model";
174 break;
176 type = "worksheet";
177 break;
178 default:
179 type = "other";
180 break;
181 }
182
183 aOut.Print( aNestLevel + 2, "(type %s)\n", type );
184
185 if( aWriteData )
186 {
187 aOut.Print( 2, "(data\n" );
188
189 size_t first = 0;
190
191 while( first < file.compressedEncodedData.length() )
192 {
193 ssize_t remaining = file.compressedEncodedData.length() - first;
194 int length = std::min( remaining, MIME_BASE64_LENGTH );
195
196 std::string_view view( file.compressedEncodedData.data() + first, length );
197
198 aOut.Print( aNestLevel + 3, "%1s%.*s%s\n", first ? "" : "|", length, view.data(),
199 remaining == length ? "|" : "" );
200 first += MIME_BASE64_LENGTH;
201 }
202 aOut.Print( aNestLevel + 2, ")\n" ); // Close data
203 }
204
205 aOut.Print( aNestLevel + 2, "(checksum \"%s\")\n", file.data_hash.c_str() );
206 aOut.Print( aNestLevel + 1, ")\n" ); // Close file
207 }
208
209 aOut.Print( aNestLevel, ")\n" ); // Close embedded_files
210}
211
212// Compress and Base64 encode data
214{
215 std::vector<char> compressedData;
216 size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
217 compressedData.resize( estCompressedSize );
218 size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
219 aFile.decompressedData.data(),
220 aFile.decompressedData.size(), 15 );
221
222 if( ZSTD_isError( compressedSize ) )
223 {
224 compressedData.clear();
226 }
227
228 const size_t dstLen = wxBase64EncodedSize( compressedSize );
229 aFile.compressedEncodedData.resize( dstLen );
230 size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
231 compressedData.data(), compressedSize );
232 if( retval != dstLen )
233 {
234 aFile.compressedEncodedData.clear();
236 }
237
239 hash.add( aFile.decompressedData );
240 aFile.data_hash = hash.digest().ToString();
241
242 return RETURN_CODE::OK;
243}
244
245// Decompress and Base64 decode data
247{
248 std::vector<char> compressedData;
249 size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
250
251 if( compressedSize == 0 )
252 {
253 wxLogTrace( wxT( "KICAD_EMBED" ),
254 wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
255 __FILE__, __FUNCTION__, __LINE__, aFile.name, aFile.compressedEncodedData.size() );
257 }
258
259 compressedData.resize( compressedSize );
260 void* compressed = compressedData.data();
261
262 // The return value from wxBase64Decode is the actual size of the decoded data avoiding
263 // the modulo 4 padding of the base64 encoding
264 compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
265
266 unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
267
268 if( estDecompressedSize > 1e9 ) // Limit to 1GB
270
271 if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
272 || estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
273 {
275 }
276
277 aFile.decompressedData.resize( estDecompressedSize );
278 void* decompressed = aFile.decompressedData.data();
279
280 size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
281 compressed, compressedSize );
282
283 if( ZSTD_isError( decompressedSize ) )
284 {
285 wxLogTrace( wxT( "KICAD_EMBED" ),
286 wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
287 __FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
288 aFile.decompressedData.clear();
290 }
291
292 aFile.decompressedData.resize( decompressedSize );
293 std::string test_hash;
294 std::string new_hash;
295
297 hash.add( aFile.decompressedData );
298 new_hash = hash.digest().ToString();
299
300 if( aFile.data_hash.length() == 64 )
301 picosha2::hash256_hex_string( aFile.decompressedData, test_hash );
302 else
303 test_hash = new_hash;
304
305 if( test_hash != aFile.data_hash )
306 {
307 wxLogTrace( wxT( "KICAD_EMBED" ),
308 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
309 __FILE__, __FUNCTION__, __LINE__, aFile.name );
310 aFile.decompressedData.clear();
312 }
313
314 aFile.data_hash = new_hash;
315
316 return RETURN_CODE::OK;
317}
318
319// Parsing method
321{
322 if( !aFiles )
323 THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
324 CurLineNumber(), CurOffset() );
325
326 using namespace EMBEDDED_FILES_T;
327
328 std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
329
330 for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
331 {
332 if( token != T_LEFT )
333 Expecting( T_LEFT );
334
335 token = NextTok();
336
337 if( token != T_file )
338 Expecting( "file" );
339
340 if( file )
341 {
342 if( !file->compressedEncodedData.empty() )
343 {
345
346 if( !file->Validate() )
347 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
348 CurLine(), CurLineNumber(), CurOffset() );
349 }
350
351 aFiles->AddFile( file.release() );
352 }
353
354 file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
355
356
357 for( token = NextTok(); token != T_RIGHT; token = NextTok() )
358 {
359 if( token != T_LEFT )
360 Expecting( T_LEFT );
361
362 token = NextTok();
363
364 switch( token )
365 {
366
367 case T_checksum:
368 NeedSYMBOLorNUMBER();
369
370 if( !IsSymbol( token ) )
371 Expecting( "checksum data" );
372
373 file->data_hash = CurStr();
374 NeedRIGHT();
375 break;
376
377 case T_data:
378 NeedBAR();
379 token = NextTok();
380
381 file->compressedEncodedData.reserve( 1 << 17 );
382
383 while( token != T_BAR )
384 {
385 if( !IsSymbol( token ) )
386 Expecting( "base64 file data" );
387
388 file->compressedEncodedData += CurStr();
389 token = NextTok();
390 }
391
392 file->compressedEncodedData.shrink_to_fit();
393
394 NeedRIGHT();
395 break;
396
397 case T_name:
398
399 if( file )
400 {
401 wxLogTrace( wxT( "KICAD_EMBED" ),
402 wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
403 }
404
405 NeedSYMBOLorNUMBER();
406
407 file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
408 file->name = CurStr();
409 NeedRIGHT();
410
411 break;
412
413 case T_type:
414
415 token = NextTok();
416
417 switch( token )
418 {
419 case T_datasheet:
421 break;
422 case T_font:
424 break;
425 case T_model:
427 break;
428 case T_worksheet:
430 break;
431 case T_other:
433 break;
434 default:
435 Expecting( "datasheet, font, model, worksheet or other" );
436 break;
437 }
438 NeedRIGHT();
439 break;
440
441 default:
442 Expecting( "checksum, data or name" );
443 }
444 }
445 }
446
447 // Add the last file in the collection
448 if( file )
449 {
450 if( !file->compressedEncodedData.empty() )
451 {
453 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
454 CurLine(), CurLineNumber(), CurOffset() );
455 }
456
457 aFiles->AddFile( file.release() );
458 }
459}
460
461
462wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
463{
464 wxFileName cacheFile;
465
466 auto it = m_files.find( aName );
467
468 if( it == m_files.end() )
469 return cacheFile;
470
471 cacheFile.AssignDir( PATHS::GetUserCachePath() );
472 cacheFile.AppendDir( wxT( "embed" ) );
473
474 if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
475 {
476 wxLogTrace( wxT( "KICAD_EMBED" ),
477 wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
478 __FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
479
480 cacheFile.SetPath( wxFileName::GetTempDir() );
481 }
482
483 wxFileName inputName( aName );
484
485 // Store the cache file name using the data hash to allow for shared data between
486 // multiple projects using the same files as well as deconflicting files with the same name
487 cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
488 cacheFile.SetExt( inputName.GetExt() );
489
490 if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
491 return cacheFile;
492
493 wxFFileOutputStream out( cacheFile.GetFullPath() );
494
495 if( !out.IsOk() )
496 {
497 cacheFile.Clear();
498 return cacheFile;
499 }
500
501 out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
502
503 return cacheFile;
504}
505
506
507const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
508{
509 return &m_fontFiles;
510}
511
512
513const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
514{
515 m_fontFiles.clear();
516
517 for( const auto& [name, entry] : m_files )
518 {
519 if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
520 m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
521 }
522
523 return &m_fontFiles;
524}
const char * name
Definition: DXF_plotter.cpp:57
void ParseEmbedded(EMBEDDED_FILES *aFiles)
std::vector< wxString > m_fontFiles
void RemoveFile(const wxString &name, bool aErase=true)
Removes a file from the collection and frees the memory.
wxFileName GetTemporaryFileName(const wxString &aName) const
const std::vector< wxString > * UpdateFontFiles()
Helper function to get a list of fonts for fontconfig to add to the library.
static RETURN_CODE DecompressAndDecode(EMBEDDED_FILE &aFile)
Takes data from the #compressedEncodedData buffer and Base64 decodes it.
bool HasFile(const wxString &name) const
void WriteEmbeddedFiles(OUTPUTFORMATTER &aOut, int aNestLevel, bool aWriteData) const
Output formatter for the embedded files.
void ClearEmbeddedFonts()
Removes all embedded fonts from the collection.
EMBEDDED_FILE * AddFile(const wxFileName &aName, bool aOverwrite)
Loads 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...
static RETURN_CODE CompressAndEncode(EMBEDDED_FILE &aFile)
Takes data from the #decompressedData buffer and compresses it using ZSTD into the #compressedEncoded...
std::map< wxString, EMBEDDED_FILE * > m_files
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:322
int PRINTF_FUNC Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition: richio.cpp:458
static bool EnsurePathExists(const wxString &aPath)
Attempts to create a given path if it does not exist.
Definition: paths.cpp:423
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition: paths.cpp:365
#define MIME_BASE64_LENGTH
#define THROW_PARSE_ERROR(aProblem, aSource, aInputLine, aLineNumber, aByteIndex)
Definition: ki_exception.h:165
std::vector< char > decompressedData
std::string ToString() const
Definition: hash_128.h:47