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
17 * along with this program. If not, see <https://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()].get();
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::shared_ptr<EMBEDDED_FILE> efile = std::make_shared<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.get();
132 m_files[aName.GetFullName()] = std::move( efile );
133
136
137 return result;
138}
139
140
142{
143 return AddFile( std::shared_ptr<EMBEDDED_FILE>( aFile ) );
144}
145
146
147EMBEDDED_FILES::EMBEDDED_FILE* EMBEDDED_FILES::AddFile( std::shared_ptr<EMBEDDED_FILE> aFile )
148{
149 if( !aFile )
150 return nullptr;
151
152 wxString name = aFile->name;
153 auto [it, inserted] = m_files.emplace( std::move( name ), std::move( aFile ) );
154
155 // Fire the callback only when the file was actually inserted; std::map::emplace silently
156 // drops duplicates and we must not announce a stored pointer that no longer matches what
157 // the collection holds.
158 if( inserted && m_fileAddedCallback )
159 m_fileAddedCallback( it->second.get() );
160
161 return it->second.get();
162}
163
164
165// Remove a file from the collection
166void EMBEDDED_FILES::RemoveFile( const wxString& name, bool aErase )
167{
168 // aErase is retained for API compatibility; with shared_ptr ownership the map entry
169 // release will free the underlying file when no other collection still references it.
170 (void) aErase;
171
172 auto it = m_files.find( name );
173
174 if( it != m_files.end() )
175 m_files.erase( it );
176}
177
178
180 const std::set<wxString>& aExcludedNames )
181{
182 // Build the filtered set before touching m_files so self-assignment stays safe.
183 std::map<wxString, std::shared_ptr<EMBEDDED_FILE>> filtered;
184
185 for( const auto& [name, file] : aSource.m_files )
186 {
187 if( aExcludedNames.count( file->name ) )
188 continue;
189
190 filtered[name] = file;
191 }
192
193 m_files = std::move( filtered );
194}
195
196
198{
199 for( auto it = m_files.begin(); it != m_files.end(); )
200 {
201 if( it->second->type == EMBEDDED_FILE::FILE_TYPE::FONT )
202 it = m_files.erase( it );
203 else
204 ++it;
205 }
206}
207
208
209// Write the collection of files to a disk file in the specified format
210void EMBEDDED_FILES::WriteEmbeddedFiles( OUTPUTFORMATTER& aOut, bool aWriteData ) const
211{
212 ssize_t MIME_BASE64_LENGTH = 76;
213 aOut.Print( "(embedded_files " );
214
215 for( const auto& [name, entry] : m_files )
216 {
217 const EMBEDDED_FILE& file = *entry;
218
219 // Skip empty files
220 if( file.compressedEncodedData.empty() )
221 {
222 continue;
223 }
224
225 aOut.Print( "(file " );
226 aOut.Print( "(name %s)", aOut.Quotew( file.name ).c_str() );
227
228 const char* type = nullptr;
229
230 switch( file.type )
231 {
232 case EMBEDDED_FILE::FILE_TYPE::DATASHEET: type = "datasheet"; break;
233 case EMBEDDED_FILE::FILE_TYPE::FONT: type = "font"; break;
234 case EMBEDDED_FILE::FILE_TYPE::MODEL: type = "model"; break;
235 case EMBEDDED_FILE::FILE_TYPE::WORKSHEET: type = "worksheet"; break;
236 default: type = "other"; break;
237 }
238
239 aOut.Print( "(type %s)", type );
240
241 if( aWriteData )
242 {
243 aOut.Print( "(data" );
244
245 size_t first = 0;
246
247 while( first < file.compressedEncodedData.length() )
248 {
249 ssize_t remaining = file.compressedEncodedData.length() - first;
250 int length = std::min( remaining, MIME_BASE64_LENGTH );
251
252 std::string_view view( file.compressedEncodedData.data() + first, length );
253
254 aOut.Print( "\n%1s%.*s%s\n", first ? "" : "|", length, view.data(),
255 remaining == length ? "|" : "" );
256 first += MIME_BASE64_LENGTH;
257 }
258
259 aOut.Print( ")" ); // Close data
260 }
261
262 aOut.Print( "(checksum %s)", aOut.Quotew( file.data_hash ).c_str() );
263 aOut.Print( ")" ); // Close file
264 }
265
266 aOut.Print( ")" ); // Close embedded_files
267}
268
269
270// Compress and Base64 encode data
272{
273 std::vector<char> compressedData;
274 size_t estCompressedSize = ZSTD_compressBound( aFile.decompressedData.size() );
275 compressedData.resize( estCompressedSize );
276 size_t compressedSize = ZSTD_compress( compressedData.data(), estCompressedSize,
277 aFile.decompressedData.data(),
278 aFile.decompressedData.size(), 15 );
279
280 if( ZSTD_isError( compressedSize ) )
281 {
282 compressedData.clear();
284 }
285
286 const size_t dstLen = wxBase64EncodedSize( compressedSize );
287 aFile.compressedEncodedData.resize( dstLen );
288 size_t retval = wxBase64Encode( aFile.compressedEncodedData.data(), dstLen,
289 compressedData.data(), compressedSize );
290
291 if( retval != dstLen )
292 {
293 aFile.compressedEncodedData.clear();
295 }
296
298 hash.add( aFile.decompressedData );
299 aFile.data_hash = hash.digest().ToString();
300
301 return RETURN_CODE::OK;
302}
303
304
305// Decompress and Base64 decode data
307{
308 std::vector<char> compressedData;
309 size_t compressedSize = wxBase64DecodedSize( aFile.compressedEncodedData.size() );
310
311 if( compressedSize == 0 )
312 {
313 wxLogTrace( wxT( "KICAD_EMBED" ),
314 wxT( "%s:%s:%d\n * Base64DecodedSize failed for file '%s' with size %zu" ),
315 __FILE__, __FUNCTION__, __LINE__, aFile.name,
316 aFile.compressedEncodedData.size() );
318 }
319
320 compressedData.resize( compressedSize );
321 void* compressed = compressedData.data();
322
323 // The return value from wxBase64Decode is the actual size of the decoded data avoiding
324 // the modulo 4 padding of the base64 encoding
325 compressedSize = wxBase64Decode( compressed, compressedSize, aFile.compressedEncodedData );
326
327 unsigned long long estDecompressedSize = ZSTD_getFrameContentSize( compressed, compressedSize );
328
329 if( estDecompressedSize > 1e9 ) // Limit to 1GB
331
332 if( estDecompressedSize == ZSTD_CONTENTSIZE_ERROR
333 || estDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN )
334 {
336 }
337
338 aFile.decompressedData.resize( estDecompressedSize );
339 void* decompressed = aFile.decompressedData.data();
340
341 size_t decompressedSize = ZSTD_decompress( decompressed, estDecompressedSize,
342 compressed, compressedSize );
343
344 if( ZSTD_isError( decompressedSize ) )
345 {
346 wxLogTrace( wxT( "KICAD_EMBED" ),
347 wxT( "%s:%s:%d\n * ZSTD_decompress failed with error '%s'" ),
348 __FILE__, __FUNCTION__, __LINE__, ZSTD_getErrorName( decompressedSize ) );
349 aFile.decompressedData.clear();
351 }
352
353 aFile.decompressedData.resize( decompressedSize );
354
356 hash.add( aFile.decompressedData );
357 std::string new_hash = hash.digest().ToString();
358
359 if( aAllowEmptyHash && aFile.data_hash.empty() )
360 {
361 wxLogTrace( wxT( "KICAD_EMBED" ), wxT( "Generated new hash for '%s'"), aFile.name );
362 }
363 else if( aFile.data_hash.length() == 64 )
364 {
365 // SHA-256 hash from older file formats
366 std::string sha_hash;
367 picosha2::hash256_hex_string( aFile.decompressedData, sha_hash );
368
369 if( sha_hash != aFile.data_hash )
370 {
371 wxLogTrace( wxT( "KICAD_EMBED" ),
372 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
373 __FILE__, __FUNCTION__, __LINE__, aFile.name );
374 aFile.decompressedData.clear();
376 }
377 }
378 else if( new_hash != aFile.data_hash )
379 {
380 // Current MMH3 hash didn't match. Try the V1 hash algorithm for files
381 // saved before the tail-byte alignment fix.
383 v1hash.addDataV1( reinterpret_cast<const uint8_t*>( aFile.decompressedData.data() ),
384 aFile.decompressedData.size() );
385 std::string v1_hash = v1hash.digest().ToString();
386
387 if( v1_hash != aFile.data_hash )
388 {
389 wxLogTrace( wxT( "KICAD_EMBED" ),
390 wxT( "%s:%s:%d\n * Checksum error in embedded file '%s'" ),
391 __FILE__, __FUNCTION__, __LINE__, aFile.name );
392 aFile.decompressedData.clear();
394 }
395 }
396
397 aFile.data_hash = new_hash;
398
399 return RETURN_CODE::OK;
400}
401
402
404 std::string& aHash )
405{
406 if( !aFileName.FileExists() )
408
409 wxFFileInputStream file( aFileName.GetFullPath() );
410
411 if( !file.IsOk() )
413
414 wxFileOffset length = file.GetLength();
415 std::vector<char> data( length );
416
417 if( !data.data() )
419
420 char* dataPtr = data.data();
421 wxFileOffset totalRead = 0;
422
423 while( !file.Eof() && totalRead < length )
424 {
425 file.Read( dataPtr, length - totalRead );
426 size_t bytesRead = file.LastRead();
427 dataPtr += bytesRead;
428 totalRead += bytesRead;
429 }
430
432 hash.add( data );
433 aHash = hash.digest().ToString();
434
435 return RETURN_CODE::OK;
436}
437
438
439// Parsing method
441{
442 // embedded files are version 20240706 and uses also Bars as separator
443 SetKnowsBar( true );
444
445 if( !aFiles )
446 THROW_PARSE_ERROR( "No embedded files object provided", CurSource(), CurLine(),
447 CurLineNumber(), CurOffset() );
448
449 using namespace EMBEDDED_FILES_T;
450
451 std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE> file( nullptr );
452
453 for( T token = NextTok(); token != T_RIGHT; token = NextTok() )
454 {
455 if( token != T_LEFT )
456 Expecting( T_LEFT );
457
458 token = NextTok();
459
460 if( token != T_file )
461 Expecting( "file" );
462
463 if( file )
464 {
465 if( !file->compressedEncodedData.empty() )
466 {
469 {
470 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name,
471 CurSource(), CurLine(), CurLineNumber(), CurOffset() );
472 }
473 }
474
475 aFiles->AddFile( file.release() );
476 }
477
478 file = std::unique_ptr<EMBEDDED_FILES::EMBEDDED_FILE>( nullptr );
479
480 for( token = NextTok(); token != T_RIGHT; token = NextTok() )
481 {
482 if( token != T_LEFT )
483 Expecting( T_LEFT );
484
485 token = NextTok();
486
487 switch( token )
488 {
489 case T_checksum:
490 if( !file )
491 Expecting( T_name );
492
493 NeedSYMBOLorNUMBER();
494
495 if( !IsSymbol( token ) )
496 Expecting( "checksum data" );
497
498 file->data_hash = CurStr();
499 NeedRIGHT();
500 break;
501
502 case T_data:
503 if( !file )
504 Expecting( T_name);
505
506 try
507 {
508 NeedBAR();
509 }
510 catch( const PARSE_ERROR& e )
511 {
512 // No data in the file -- due to bug in writer for 9.0.0
513 if( curTok == T_RIGHT )
514 break;
515 else
516 throw e;
517 }
518 catch( ... )
519 {
520 throw;
521 }
522
523 token = NextTok();
524
525 file->compressedEncodedData.reserve( 1 << 17 );
526
527 while( token != T_BAR )
528 {
529 if( !IsSymbol( token ) )
530 Expecting( "base64 file data" );
531
532 file->compressedEncodedData += CurStr();
533 token = NextTok();
534 }
535
536 file->compressedEncodedData.shrink_to_fit();
537
538 NeedRIGHT();
539 break;
540
541 case T_name:
542 if( file )
543 {
544 wxLogTrace( wxT( "KICAD_EMBED" ),
545 wxT( "Duplicate 'name' tag in embedded file %s" ), file->name );
546 }
547
548 NeedSYMBOLorNUMBER();
549
550 file = std::make_unique<EMBEDDED_FILES::EMBEDDED_FILE>();
551 file->name = CurStr();
552 NeedRIGHT();
553
554 break;
555
556 case T_type:
557 if( !file )
558 Expecting( T_name );
559
560 token = NextTok();
561
562 switch( token )
563 {
564 case T_datasheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::DATASHEET; break;
565 case T_font: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::FONT; break;
566 case T_model: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::MODEL; break;
567 case T_worksheet: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::WORKSHEET; break;
568 case T_other: file->type = EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::OTHER; break;
569 default: Expecting( "datasheet, font, model, worksheet or other" ); break;
570 }
571
572 NeedRIGHT();
573 break;
574
575 default:
576 Expecting( "checksum, data or name" );
577 }
578 }
579 }
580
581 // Add the last file in the collection
582 if( file )
583 {
584 if( !file->compressedEncodedData.empty() )
585 {
588 {
589 THROW_PARSE_ERROR( "Checksum error in embedded file " + file->name,
590 CurSource(), CurLine(), CurLineNumber(), CurOffset() );
591 }
592 }
593
594 aFiles->AddFile( file.release() );
595 }
596}
597
598
599wxFileName EMBEDDED_FILES::GetTemporaryFileName( const wxString& aName ) const
600{
601 wxFileName cacheFile;
602
603 auto it = m_files.find( aName );
604
605 if( it == m_files.end() )
606 return cacheFile;
607
608 cacheFile.AssignDir( PATHS::GetUserCachePath() );
609 cacheFile.AppendDir( wxT( "embed" ) );
610
611 if( !PATHS::EnsurePathExists( cacheFile.GetFullPath() ) )
612 {
613 wxLogTrace( wxT( "KICAD_EMBED" ),
614 wxT( "%s:%s:%d\n * failed to create embed cache directory '%s'" ),
615 __FILE__, __FUNCTION__, __LINE__, cacheFile.GetPath() );
616
617 cacheFile.SetPath( wxFileName::GetTempDir() );
618 }
619
620 wxFileName inputName( aName );
621
622 // Store the cache file name using the data hash to allow for shared data between
623 // multiple projects using the same files as well as deconflicting files with the same name
624 cacheFile.SetName( "kicad_embedded_" + it->second->data_hash );
625 cacheFile.SetExt( inputName.GetExt() );
626
627 if( cacheFile.FileExists() && cacheFile.IsFileReadable() )
628 return cacheFile;
629
630 wxFFileOutputStream out( cacheFile.GetFullPath() );
631
632 if( !out.IsOk() )
633 {
634 cacheFile.Clear();
635 return cacheFile;
636 }
637
638 out.Write( it->second->decompressedData.data(), it->second->decompressedData.size() );
639
640 return cacheFile;
641}
642
643
644const std::vector<wxString>* EMBEDDED_FILES::GetFontFiles() const
645{
646 return &m_fontFiles;
647}
648
649
650const std::vector<wxString>* EMBEDDED_FILES::UpdateFontFiles()
651{
652 m_fontFiles.clear();
653
654 for( const auto& [name, entry] : m_files )
655 {
656 if( entry->type == EMBEDDED_FILE::FILE_TYPE::FONT )
657 m_fontFiles.push_back( GetTemporaryFileName( name ).GetFullPath() );
658 }
659
660 return &m_fontFiles;
661}
662
663
664// Move constructor
666 m_files( std::move( other.m_files ) ),
667 m_fontFiles( std::move( other.m_fontFiles ) ),
668 m_fileAddedCallback( std::move( other.m_fileAddedCallback ) ),
669 m_embedFonts( other.m_embedFonts )
670{
671 other.m_embedFonts = false;
672}
673
674
675// Move assignment operator
677{
678 if (this != &other)
679 {
681 m_files = std::move( other.m_files );
682 m_fontFiles = std::move( other.m_fontFiles );
683 m_fileAddedCallback = std::move( other.m_fileAddedCallback );
684 m_embedFonts = other.m_embedFonts;
685 other.m_embedFonts = false;
686 }
687
688 return *this;
689}
690
691
692// Copy constructor
693//
694// Shares ownership of the underlying EMBEDDED_FILE payloads via shared_ptr so that cloning a
695// container that transitively holds embedded files (e.g. a FOOTPRINT being snapshotted into the
696// undo stack) does not duplicate large blobs such as embedded 3D models. Mutations applied
697// through raw EMBEDDED_FILE* pointers obtained from one collection will be visible to other
698// collections that share the same shared_ptr; callers that require an isolated mutable copy
699// should construct a new EMBEDDED_FILE explicitly.
707
708
709EMBEDDED_FILES::EMBEDDED_FILES( const EMBEDDED_FILES& other, bool aDeepCopy ) :
712{
713 if( aDeepCopy )
714 {
715 // True deep copy is requested. Allocate a fresh EMBEDDED_FILE for each entry so that
716 // subsequent mutations through this collection cannot affect the source collection.
717 for( const auto& [name, file] : other.m_files )
718 m_files[name] = std::make_shared<EMBEDDED_FILE>( *file );
719
720 m_fontFiles = other.m_fontFiles;
721 }
722}
723
724
725// Copy assignment operator
726//
727// Assignment performs a deep copy (in contrast to the copy constructor, which shares ownership
728// of payloads). Assignment is used by callers such as FOOTPRINT::operator= and
729// LIB_SYMBOL::operator= where the destination is a live, separately editable object that may
730// later mutate EMBEDDED_FILE fields through raw pointers; aliasing would let those mutations
731// silently bleed into the source. The cheap-clone path for FOOTPRINT::Clone() goes through the
732// copy constructor instead.
734{
735 if( this != &other )
736 {
737 m_files.clear();
738
739 for( const auto& [name, file] : other.m_files )
740 m_files[name] = std::make_shared<EMBEDDED_FILE>( *file );
741
742 m_fontFiles = other.m_fontFiles;
745 }
746
747 return *this;
748}
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.
static RETURN_CODE DecompressAndDecode(EMBEDDED_FILE &aFile, bool aAllowEmptyHash=false)
Takes data from the #compressedEncodedData buffer and Base64 decodes it.
@ 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 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...
std::map< wxString, std::shared_ptr< EMBEDDED_FILE > > m_files
EMBEDDED_FILES()=default
static RETURN_CODE CompressAndEncode(EMBEDDED_FILE &aFile)
Take data from the #decompressedData buffer and compresses it using ZSTD into the #compressedEncodedD...
bool m_embedFonts
If set, fonts will be embedded in the element on save.
void AssignSharedFrom(const EMBEDDED_FILES &aSource, const std::set< wxString > &aExcludedNames={})
Replace this collection's files with references to aSource's files, skipping any whose name appears i...
A streaming C++ equivalent for MurmurHash3_x64_128.
Definition mmh3_hash.h:56
FORCE_INLINE void addDataV1(const uint8_t *data, size_t length)
Definition mmh3_hash.h:95
FORCE_INLINE void add(const std::string &input)
Definition mmh3_hash.h:117
FORCE_INLINE HASH_128 digest()
Definition mmh3_hash.h:136
An interface used to output 8 bit text in a convenient way.
Definition richio.h:294
std::string Quotew(const wxString &aWrapee) const
Definition richio.cpp:505
int PRINTF_FUNC_N Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition richio.cpp:432
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:460
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:43
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.