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