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/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 m_files[aName.GetFullName()] = efile.release();
105
106 return m_files[aName.GetFullName()];
107}
108
109
111{
112 m_files.insert( { aFile->name, aFile } );
113}
114
115// Remove a file from the collection
116void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
117{
118 auto it = m_files.find( name );
119
120 if( it != m_files.end() )
121 {
122 m_files.erase( it );
123
124 if( aErase )
125 delete it->second;
126 }
127}
128
129
131{
132 for( auto it = m_files.begin(); it != m_files.end(); )
133 {
134 if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
135 {
136 delete it->second;
137 it = m_files.erase( it );
138 }
139 else
140 {
141 ++it;
142 }
143 }
144}
145
146
147// Write the collection of files to a disk file in the specified format
148void EMBEDDED_FILES::WriteEmbeddedFiles( OUTPUTFORMATTER& aOut, bool aWriteData ) const
149{
150 ssize_t MIME_BASE64_LENGTH = 76;
151 aOut.Print( "(embedded_files " );
152
153 for( const auto& [name, entry] : m_files )
154 {
155 const EMBEDDED_FILE& file = *entry;
156
157 aOut.Print( "(file " );
158 aOut.Print( "(name %s)", aOut.Quotew( file.name ).c_str() );
159
160 const char* type = nullptr;
161
162 switch( file.type )
163 {
164 case EMBEDDED_FILE::FILE_TYPE::DATASHEET: type = "datasheet"; break;
165 case EMBEDDED_FILE::FILE_TYPE::FONT: type = "font"; break;
166 case EMBEDDED_FILE::FILE_TYPE::MODEL: type = "model"; break;
167 case EMBEDDED_FILE::FILE_TYPE::WORKSHEET: type = "worksheet"; break;
168 default: type = "other"; break;
169 }
170
171 aOut.Print( "(type %s)", type );
172
173 if( aWriteData )
174 {
175 aOut.Print( "(data" );
176
177 size_t first = 0;
178
179 while( first < file.compressedEncodedData.length() )
180 {
181 ssize_t remaining = file.compressedEncodedData.length() - first;
182 int length = std::min( remaining, MIME_BASE64_LENGTH );
183
184 std::string_view view( file.compressedEncodedData.data() + first, length );
185
186 aOut.Print( "\n%1s%.*s%s\n", first ? "" : "|", length, view.data(),
187 remaining == length ? "|" : "" );
188 first += MIME_BASE64_LENGTH;
189 }
190 aOut.Print( ")" ); // Close data
191 }
192
193 aOut.Print( "(checksum %s)", aOut.Quotew( file.data_hash ).c_str() );
194 aOut.Print( ")" ); // Close file
195 }
196
197 aOut.Print( ")" ); // Close embedded_files
198}
199
200// Compress and Base64 encode data
202{
203 std::vector<char> compressedData;
204 size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
205 compressedData.resize( estCompressedSize );
206 size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
207 aFile.decompressedData.data(),
208 aFile.decompressedData.size(), 15 );
209
210 if( ZSTD_isError( compressedSize ) )
211 {
212 compressedData.clear();
214 }
215
216 const size_t dstLen = wxBase64EncodedSize( compressedSize );
217 aFile.compressedEncodedData.resize( dstLen );
218 size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
219 compressedData.data(), compressedSize );
220 if( retval != dstLen )
221 {
222 aFile.compressedEncodedData.clear();
224 }
225
227 hash.add( aFile.decompressedData );
228 aFile.data_hash = hash.digest().ToString();
229
230 return RETURN_CODE::OK;
231}
232
233// Decompress and Base64 decode data
235{
236 std::vector<char> compressedData;
237 size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
238
239 if( compressedSize == 0 )
240 {
241 wxLogTrace( wxT( "KICAD_EMBED" ),
242 wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
243 __FILE__, __FUNCTION__, __LINE__, aFile.name, aFile.compressedEncodedData.size() );
245 }
246
247 compressedData.resize( compressedSize );
248 void* compressed = compressedData.data();
249
250 // The return value from wxBase64Decode is the actual size of the decoded data avoiding
251 // the modulo 4 padding of the base64 encoding
252 compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
253
254 unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
255
256 if( estDecompressedSize > 1e9 ) // Limit to 1GB
258
259 if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
260 || estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
261 {
263 }
264
265 aFile.decompressedData.resize( estDecompressedSize );
266 void* decompressed = aFile.decompressedData.data();
267
268 size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
269 compressed, compressedSize );
270
271 if( ZSTD_isError( decompressedSize ) )
272 {
273 wxLogTrace( wxT( "KICAD_EMBED" ),
274 wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
275 __FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
276 aFile.decompressedData.clear();
278 }
279
280 aFile.decompressedData.resize( decompressedSize );
281 std::string test_hash;
282 std::string new_hash;
283
285 hash.add( aFile.decompressedData );
286 new_hash = hash.digest().ToString();
287
288 if( aFile.data_hash.length() == 64 )
289 picosha2::hash256_hex_string( aFile.decompressedData, test_hash );
290 else
291 test_hash = new_hash;
292
293 if( test_hash != aFile.data_hash )
294 {
295 wxLogTrace( wxT( "KICAD_EMBED" ),
296 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
297 __FILE__, __FUNCTION__, __LINE__, aFile.name );
298 aFile.decompressedData.clear();
300 }
301
302 aFile.data_hash = new_hash;
303
304 return RETURN_CODE::OK;
305}
306
307// Parsing method
309{
310 if( !aFiles )
311 THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
312 CurLineNumber(), CurOffset() );
313
314 using namespace EMBEDDED_FILES_T;
315
316 std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
317
318 for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
319 {
320 if( token != T_LEFT )
321 Expecting( T_LEFT );
322
323 token = NextTok();
324
325 if( token != T_file )
326 Expecting( "file" );
327
328 if( file )
329 {
330 if( !file->compressedEncodedData.empty() )
331 {
333
334 if( !file->Validate() )
335 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
336 CurLine(), CurLineNumber(), CurOffset() );
337 }
338
339 aFiles->AddFile( file.release() );
340 }
341
342 file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
343
344
345 for( token = NextTok(); token != T_RIGHT; token = NextTok() )
346 {
347 if( token != T_LEFT )
348 Expecting( T_LEFT );
349
350 token = NextTok();
351
352 switch( token )
353 {
354
355 case T_checksum:
356 NeedSYMBOLorNUMBER();
357
358 if( !IsSymbol( token ) )
359 Expecting( "checksum data" );
360
361 file->data_hash = CurStr();
362 NeedRIGHT();
363 break;
364
365 case T_data:
366 NeedBAR();
367 token = NextTok();
368
369 file->compressedEncodedData.reserve( 1 << 17 );
370
371 while( token != T_BAR )
372 {
373 if( !IsSymbol( token ) )
374 Expecting( "base64 file data" );
375
376 file->compressedEncodedData += CurStr();
377 token = NextTok();
378 }
379
380 file->compressedEncodedData.shrink_to_fit();
381
382 NeedRIGHT();
383 break;
384
385 case T_name:
386
387 if( file )
388 {
389 wxLogTrace( wxT( "KICAD_EMBED" ),
390 wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
391 }
392
393 NeedSYMBOLorNUMBER();
394
395 file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
396 file->name = CurStr();
397 NeedRIGHT();
398
399 break;
400
401 case T_type:
402
403 token = NextTok();
404
405 switch( token )
406 {
407 case T_datasheet:
409 break;
410 case T_font:
412 break;
413 case T_model:
415 break;
416 case T_worksheet:
418 break;
419 case T_other:
421 break;
422 default:
423 Expecting( "datasheet, font, model, worksheet or other" );
424 break;
425 }
426 NeedRIGHT();
427 break;
428
429 default:
430 Expecting( "checksum, data or name" );
431 }
432 }
433 }
434
435 // Add the last file in the collection
436 if( file )
437 {
438 if( !file->compressedEncodedData.empty() )
439 {
441 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name, CurSource(),
442 CurLine(), CurLineNumber(), CurOffset() );
443 }
444
445 aFiles->AddFile( file.release() );
446 }
447}
448
449
450wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
451{
452 wxFileName cacheFile;
453
454 auto it = m_files.find( aName );
455
456 if( it == m_files.end() )
457 return cacheFile;
458
459 cacheFile.AssignDir( PATHS::GetUserCachePath() );
460 cacheFile.AppendDir( wxT( "embed" ) );
461
462 if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
463 {
464 wxLogTrace( wxT( "KICAD_EMBED" ),
465 wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
466 __FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
467
468 cacheFile.SetPath( wxFileName::GetTempDir() );
469 }
470
471 wxFileName inputName( aName );
472
473 // Store the cache file name using the data hash to allow for shared data between
474 // multiple projects using the same files as well as deconflicting files with the same name
475 cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
476 cacheFile.SetExt( inputName.GetExt() );
477
478 if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
479 return cacheFile;
480
481 wxFFileOutputStream out( cacheFile.GetFullPath() );
482
483 if( !out.IsOk() )
484 {
485 cacheFile.Clear();
486 return cacheFile;
487 }
488
489 out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
490
491 return cacheFile;
492}
493
494
495const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
496{
497 return &m_fontFiles;
498}
499
500
501const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
502{
503 m_fontFiles.clear();
504
505 for( const auto& [name, entry] : m_files )
506 {
507 if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
508 m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
509 }
510
511 return &m_fontFiles;
512}
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
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.
static RETURN_CODE DecompressAndDecode(EMBEDDED_FILE &aFile)
Takes data from the #compressedEncodedData buffer and Base64 decodes it.
bool HasFile(const wxString &name) const
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
std::string Quotew(const wxString &aWrapee) const
Definition: richio.cpp:543
int PRINTF_FUNC_N 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