KiCad PCB EDA Suite
pcm_task_manager.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) 2021 Andrew Lutsenko, anlutsenko at gmail dot com
5 * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20// kicad_curl_easy.h **must be** included before any wxWidgets header to avoid conflicts
21// at least on Windows/msys2
24
25#include <paths.h>
26#include "pcm_task_manager.h"
27#include <reporter.h>
28#include <wxstream_helper.h>
29
30#include <fstream>
31#include <thread>
32#include <unordered_set>
33#include <wx/dir.h>
34#include <wx/filename.h>
35#include <wx/msgdlg.h>
36#include <wx/sstream.h>
37#include <wx/wfstream.h>
38#include <wx/zipstrm.h>
39
40
42 std::forward_list<wxRegEx>& aKeepOnUpdate )
43{
44 auto compile_regex = [&]( const wxString& regex )
45 {
46 aKeepOnUpdate.emplace_front( regex, wxRE_DEFAULT );
47
48 if( !aKeepOnUpdate.front().IsValid() )
49 aKeepOnUpdate.pop_front();
50 };
51
52 std::for_each( pkg.keep_on_update.begin(), pkg.keep_on_update.end(), compile_regex );
53 std::for_each( ver.keep_on_update.begin(), ver.keep_on_update.end(), compile_regex );
54}
55
56
57void PCM_TASK_MANAGER::DownloadAndInstall( const PCM_PACKAGE& aPackage, const wxString& aVersion,
58 const wxString& aRepositoryId, const bool isUpdate )
59{
60 PCM_TASK download_task = [aPackage, aVersion, aRepositoryId, isUpdate, this]()
61 {
62 wxFileName file_path( PATHS::GetUserCachePath(), "" );
63 file_path.AppendDir( "pcm" );
64 file_path.SetFullName( wxString::Format( "%s_v%s.zip", aPackage.identifier, aVersion ) );
65
66 auto find_pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
67 [&aVersion]( const PACKAGE_VERSION& pv )
68 {
69 return pv.version == aVersion;
70 } );
71
72 if( find_pkgver == aPackage.versions.end() )
73 {
74 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
75 aVersion, aPackage.identifier ),
77 return;
78 }
79
80 if( !wxDirExists( file_path.GetPath() )
81 && !wxFileName::Mkdir( file_path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
82 {
83 m_reporter->PCMReport( _( "Unable to create download directory!" ),
85 return;
86 }
87
88 int code = downloadFile( file_path.GetFullPath(), *find_pkgver->download_url );
89
90 if( code != CURLE_OK )
91 {
92 // Cleanup after ourselves and exit
93 wxRemoveFile( file_path.GetFullPath() );
94 return;
95 }
96
97 PCM_TASK install_task = [aPackage, aVersion, aRepositoryId, file_path, isUpdate, this]()
98 {
99 installDownloadedPackage( aPackage, aVersion, aRepositoryId, file_path, isUpdate );
100 };
101
102 m_install_queue.push( install_task );
103 };
104
105 m_download_queue.push( download_task );
106}
107
108
109int PCM_TASK_MANAGER::downloadFile( const wxString& aFilePath, const wxString& url )
110{
111 TRANSFER_CALLBACK callback = [&]( size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow )
112 {
113 if( dltotal > 1024 )
114 m_reporter->SetDownloadProgress( dlnow, dltotal );
115 else
116 m_reporter->SetDownloadProgress( 0.0, 0.0 );
117
118 return m_reporter->IsCancelled();
119 };
120
121 std::ofstream out( aFilePath.ToUTF8(), std::ofstream::binary );
122
123 KICAD_CURL_EASY curl;
124 curl.SetOutputStream( &out );
125 curl.SetURL( url.ToUTF8().data() );
126 curl.SetFollowRedirects( true );
127 curl.SetTransferCallback( callback, 250000L );
128
129 m_reporter->PCMReport( wxString::Format( _( "Downloading package url: '%s'" ), url ),
131
132 int code = curl.Perform();
133
134 out.close();
135
136 uint64_t download_total;
137
138 if( CURLE_OK == curl.GetTransferTotal( download_total ) )
139 m_reporter->SetDownloadProgress( download_total, download_total );
140
141 if( code != CURLE_OK && code != CURLE_ABORTED_BY_CALLBACK )
142 {
143 m_reporter->PCMReport( wxString::Format( _( "Failed to download url %s\n%s" ), url,
144 curl.GetErrorText( code ) ),
146 }
147
148 return code;
149}
150
151
153 const wxString& aVersion,
154 const wxString& aRepositoryId,
155 const wxFileName& aFilePath, const bool isUpdate )
156{
157 auto pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
158 [&aVersion]( const PACKAGE_VERSION& pv )
159 {
160 return pv.version == aVersion;
161 } );
162
163 if( pkgver == aPackage.versions.end() )
164 {
165 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
166 aVersion, aPackage.identifier ),
168 return;
169 }
170
171 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
172 std::forward_list<wxRegEx> keep_on_update;
173
174 if( isUpdate )
175 compile_keep_on_update_regex( aPackage, *pkgver, keep_on_update );
176
177 const std::optional<wxString>& hash = pkgver->download_sha256;
178 bool hash_match = true;
179
180 if( hash )
181 {
182 std::ifstream stream( aFilePath.GetFullPath().ToUTF8(), std::ios::binary );
183 hash_match = m_pcm->VerifyHash( stream, *hash );
184 }
185
186 if( !hash_match )
187 {
188 m_reporter->PCMReport( wxString::Format( _( "Downloaded archive hash for package "
189 "%s does not match repository entry. "
190 "This may indicate a problem with the "
191 "package, if the issue persists "
192 "report this to repository maintainers." ),
193 aPackage.identifier ),
195 }
196 else
197 {
198 if( isUpdate )
199 {
200 m_reporter->PCMReport(
201 wxString::Format( _( "Removing previous version of package '%s'." ),
202 aPackage.identifier ),
204
205 deletePackageDirectories( aPackage.identifier, keep_on_update );
206 }
207
208 m_reporter->PCMReport(
209 wxString::Format( _( "Extracting package '%s'." ), aPackage.identifier ),
211
212 if( extract( aFilePath.GetFullPath(), aPackage.identifier, true ) )
213 {
214 m_pcm->MarkInstalled( aPackage, pkgver->version, aRepositoryId );
215 }
216 else
217 {
218 // Cleanup possibly partially extracted package
220 }
221
222 std::unique_lock lock( m_changed_package_types_guard );
223 m_changed_package_types.insert( aPackage.type );
224 }
225
226 m_reporter->PCMReport(
227 wxString::Format( _( "Removing downloaded archive '%s'." ), aFilePath.GetFullName() ),
229
230 wxRemoveFile( aFilePath.GetFullPath() );
231}
232
233
234bool PCM_TASK_MANAGER::extract( const wxString& aFilePath, const wxString& aPackageId,
235 bool isMultiThreaded )
236{
237 wxFFileInputStream stream( aFilePath );
238 wxZipInputStream zip( stream );
239
240 wxLogNull no_wx_logging;
241
242 int entries = zip.GetTotalEntries();
243 int extracted = 0;
244
245 wxArchiveEntry* entry = zip.GetNextEntry();
246
247 if( !zip.IsOk() )
248 {
249 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
250 return false;
251 }
252
253 // Namespace delimiter changed on disk to allow flat loading of Python modules
254 wxString clean_package_id = aPackageId;
255 clean_package_id.Replace( '.', '_' );
256
257 for( ; entry; entry = zip.GetNextEntry() )
258 {
259 wxArrayString path_parts =
260 wxSplit( entry->GetName(), wxFileName::GetPathSeparator(), (wxChar) 0 );
261
262 if( path_parts.size() < 2
263 || PCM_PACKAGE_DIRECTORIES.find( path_parts[0] ) == PCM_PACKAGE_DIRECTORIES.end()
264 || path_parts[path_parts.size() - 1].IsEmpty() )
265 {
266 // Ignore files in the root of the archive and files outside of package dirs.
267 continue;
268 }
269
270 // m_reporter->Report( wxString::Format( _( "Extracting file '%s'\n" ), entry->GetName() ),
271 // RPT_SEVERITY_INFO );
272
273 // Transform paths from
274 // <PackageRoot>/$folder/$contents
275 // To
276 // $KICAD7_3RD_PARTY/$folder/$package_id/$contents
277 path_parts.Insert( clean_package_id, 1 );
278 path_parts.Insert( m_pcm->Get3rdPartyPath(), 0 );
279
280 wxString fullname = wxJoin( path_parts, wxFileName::GetPathSeparator(), (wxChar) 0 );
281
282 // Ensure the target directory exists and create it if not.
283 wxString t_path = wxPathOnly( fullname );
284
285 if( !wxDirExists( t_path ) )
286 {
287 wxFileName::Mkdir( t_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
288 }
289
290 wxTempFileOutputStream out( fullname );
291
292 if( !( CopyStreamData( zip, out, entry->GetSize() ) && out.Commit() ) )
293 {
294 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
295 return false;
296 }
297
298 extracted++;
299 m_reporter->SetPackageProgress( extracted, entries );
300
301 if( !isMultiThreaded )
302 m_reporter->KeepRefreshing( false );
303
304 if( m_reporter->IsCancelled() )
305 break;
306 }
307
308 zip.CloseEntry();
309
310 if( m_reporter->IsCancelled() )
311 {
312 m_reporter->PCMReport( _( "Aborting package installation." ), RPT_SEVERITY_INFO );
313 return false;
314 }
315
316 m_reporter->PCMReport( _( "Extracted package\n" ), RPT_SEVERITY_INFO );
317 m_reporter->SetPackageProgress( entries, entries );
318
319 return true;
320}
321
322
323void PCM_TASK_MANAGER::InstallFromFile( wxWindow* aParent, const wxString& aFilePath )
324{
325 wxFFileInputStream stream( aFilePath );
326
327 if( !stream.IsOk() )
328 {
329 wxLogError( _( "Could not open archive file." ) );
330 return;
331 }
332
333 wxZipInputStream zip( stream );
334
335 if( !zip.IsOk() )
336 {
337 wxLogError( _( "Invalid archive file format." ) );
338 return;
339 }
340
341 nlohmann::json metadata;
342
343 for( wxArchiveEntry* entry = zip.GetNextEntry(); entry != nullptr; entry = zip.GetNextEntry() )
344 {
345 // Find and load metadata.json
346 if( entry->GetName() != "metadata.json" )
347 continue;
348
349 wxStringOutputStream strStream;
350
351 if( CopyStreamData( zip, strStream, entry->GetSize() ) )
352 {
353 try
354 {
355 metadata = nlohmann::json::parse( strStream.GetString().ToUTF8().data() );
356 m_pcm->ValidateJson( metadata );
357 }
358 catch( const std::exception& e )
359 {
360 wxLogError( wxString::Format( _( "Unable to parse package metadata:\n\n%s" ),
361 e.what() ) );
362 break;
363 }
364 }
365 }
366
367 if( metadata.empty() )
368 {
369 wxLogError( _( "Archive does not contain a valid metadata.json file" ) );
370 return;
371 }
372
373 PCM_PACKAGE package = metadata.get<PCM_PACKAGE>();
374
375 if( package.versions.size() != 1 )
376 {
377 wxLogError( _( "Archive metadata must have a single version defined" ) );
378 return;
379 }
380
381 bool isUpdate = false;
382 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
383 std::forward_list<wxRegEx> keep_on_update;
384 const std::vector<PCM_INSTALLATION_ENTRY> installed_packages = m_pcm->GetInstalledPackages();
385
386 if( std::find_if( installed_packages.begin(), installed_packages.end(),
387 [&]( const PCM_INSTALLATION_ENTRY& entry )
388 {
389 return entry.package.identifier == package.identifier;
390 } )
391 != installed_packages.end() )
392 {
393 if( wxMessageBox(
395 _( "Package with identifier %s is already installed. "
396 "Would you like to update it to the version from selected file?" ),
397 package.identifier ),
398 _( "Update package" ), wxICON_EXCLAMATION | wxYES_NO, aParent )
399 == wxNO )
400 return;
401
402 isUpdate = true;
403
404 compile_keep_on_update_regex( package, package.versions[0], keep_on_update );
405 }
406
407 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent, false );
408#ifdef __WXMAC__
409 m_reporter->ShowWindowModal();
410#else
411 m_reporter->Show();
412#endif
413
414 if( isUpdate )
415 {
416 m_reporter->PCMReport( wxString::Format( _( "Removing previous version of package '%s'." ),
417 package.identifier ),
419
420 deletePackageDirectories( package.identifier, keep_on_update );
421 }
422
423 if( extract( aFilePath, package.identifier, false ) )
424 m_pcm->MarkInstalled( package, package.versions[0].version, "" );
425
426 m_reporter->SetFinished();
427 m_reporter->KeepRefreshing( false );
428 m_reporter->Destroy();
429 m_reporter.reset();
430
431 aParent->Raise();
432
433 std::unique_lock lock( m_changed_package_types_guard );
434 m_changed_package_types.insert( package.type );
435}
436
437
438class PATH_COLLECTOR : public wxDirTraverser
439{
440private:
441 std::vector<wxString>& m_files;
442 std::vector<wxString>& m_dirs;
443
444public:
445 explicit PATH_COLLECTOR( std::vector<wxString>& aFiles, std::vector<wxString>& aDirs ) :
446 m_files( aFiles ), m_dirs( aDirs )
447 {
448 }
449
450 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
451 {
452 m_files.push_back( aFilePath );
453 return wxDIR_CONTINUE;
454 }
455
456 wxDirTraverseResult OnDir( const wxString& dirPath ) override
457 {
458 m_dirs.push_back( dirPath );
459 return wxDIR_CONTINUE;
460 }
461};
462
463
464void PCM_TASK_MANAGER::deletePackageDirectories( const wxString& aPackageId,
465 const std::forward_list<wxRegEx>& aKeep )
466{
467 // Namespace delimiter changed on disk to allow flat loading of Python modules
468 wxString clean_package_id = aPackageId;
469 clean_package_id.Replace( '.', '_' );
470
471 int path_prefix_len = m_pcm->Get3rdPartyPath().Length();
472
473 auto sort_func = []( const wxString& a, const wxString& b )
474 {
475 if( a.length() > b.length() )
476 return true;
477 if( a.length() < b.length() )
478 return false;
479
480 if( a != b )
481 return a < b;
482
483 return false;
484 };
485
486 for( const wxString& dir : PCM_PACKAGE_DIRECTORIES )
487 {
488 wxFileName d( m_pcm->Get3rdPartyPath(), "" );
489 d.AppendDir( dir );
490 d.AppendDir( clean_package_id );
491
492 if( !d.DirExists() )
493 continue;
494
495 m_reporter->PCMReport( wxString::Format( _( "Removing directory %s" ), d.GetPath() ),
497
498 if( aKeep.empty() )
499 {
500 if( !d.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
501 {
502 m_reporter->PCMReport(
503 wxString::Format( _( "Failed to remove directory %s" ), d.GetPath() ),
505 }
506 }
507 else
508 {
509 std::vector<wxString> files;
510 std::vector<wxString> dirs;
511 PATH_COLLECTOR collector( files, dirs );
512
513 wxDir( d.GetFullPath() )
514 .Traverse( collector, wxEmptyString, wxDIR_DEFAULT | wxDIR_NO_FOLLOW );
515
516 // Do a poor mans post order traversal by sorting paths in reverse length order
517 std::sort( files.begin(), files.end(), sort_func );
518 std::sort( dirs.begin(), dirs.end(), sort_func );
519
520 // Delete files that don't match any of the aKeep regexs
521 for( const wxString& file : files )
522 {
523 bool del = true;
524
525 for( const wxRegEx& re : aKeep )
526 {
527 wxString tmp = file.Mid( path_prefix_len );
528 tmp.Replace( "\\", "/" );
529
530 if( re.Matches( tmp ) )
531 {
532 // m_reporter->PCMReport( wxString::Format( _( "Keeping file '%s'." ), tmp ),
533 // RPT_SEVERITY_INFO );
534
535 del = false;
536 break;
537 }
538 }
539
540 if( del )
541 wxRemoveFile( file );
542 }
543
544 // Delete any empty dirs
545 for( const wxString& empty_dir : dirs )
546 {
547 wxFileName dname( empty_dir, "" );
548 dname.Rmdir(); // not passing any flags here will only remove empty directories
549 }
550 }
551 }
552}
553
554
556{
557 PCM_TASK task = [aPackage, this]
558 {
560
561 m_pcm->MarkUninstalled( aPackage );
562
563 std::unique_lock lock( m_changed_package_types_guard );
564 m_changed_package_types.insert( aPackage.type );
565
566 m_reporter->PCMReport(
567 wxString::Format( _( "Package %s uninstalled" ), aPackage.identifier ),
569 };
570
571 m_install_queue.push( task );
572}
573
574
575void PCM_TASK_MANAGER::RunQueue( wxWindow* aParent )
576{
577 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent );
578
579 m_reporter->SetNumPhases( m_download_queue.size() + m_install_queue.size() );
580#ifdef __WXMAC__
581 m_reporter->ShowWindowModal();
582#else
583 m_reporter->Show();
584#endif
585
586 wxSafeYield();
587
588 std::mutex mutex;
589 std::condition_variable condvar;
590 bool download_complete = false;
591
592 std::thread download_thread(
593 [&]()
594 {
595 while( !m_download_queue.empty() && !m_reporter->IsCancelled() )
596 {
597 PCM_TASK task;
598 m_download_queue.pop( task );
599 task();
600 condvar.notify_all();
601 }
602
603 std::unique_lock<std::mutex> lock( mutex );
604 download_complete = true;
605 condvar.notify_all();
606 } );
607
608 std::thread install_thread(
609 [&]()
610 {
611 std::unique_lock<std::mutex> lock( mutex );
612
613 do
614 {
615 condvar.wait( lock,
616 [&]()
617 {
618 return download_complete || !m_install_queue.empty()
619 || m_reporter->IsCancelled();
620 } );
621
622 lock.unlock();
623
624 while( !m_install_queue.empty() && !m_reporter->IsCancelled() )
625 {
626 PCM_TASK task;
627 m_install_queue.pop( task );
628 task();
629 m_reporter->AdvancePhase();
630 }
631
632 lock.lock();
633
634 } while( ( !m_install_queue.empty() || !download_complete )
635 && !m_reporter->IsCancelled() );
636
637 m_reporter->PCMReport( _( "Done." ), RPT_SEVERITY_INFO );
638
639 m_reporter->SetFinished();
640 } );
641
642 m_reporter->KeepRefreshing( true );
643
644 download_thread.join();
645 install_thread.join();
646
647 // Destroy the reporter only after the threads joined
648 // Incase the reporter terminated due to cancellation
649 m_reporter->Destroy();
650 m_reporter.reset();
651
652 aParent->Raise();
653}
int Perform()
Equivalent to curl_easy_perform.
bool SetTransferCallback(const TRANSFER_CALLBACK &aCallback, size_t aInterval)
bool SetURL(const std::string &aURL)
Set the request URL.
bool SetFollowRedirects(bool aFollow)
Enable the following of HTTP(s) and other redirects, by default curl does not follow redirects.
int GetTransferTotal(uint64_t &aDownloadedBytes) const
bool SetOutputStream(const std::ostream *aOutput)
const std::string GetErrorText(int aCode)
Fetch CURL's "friendly" error string for a given error code.
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition: paths.cpp:321
PATH_COLLECTOR(std::vector< wxString > &aFiles, std::vector< wxString > &aDirs)
wxDirTraverseResult OnDir(const wxString &dirPath) override
std::vector< wxString > & m_dirs
std::vector< wxString > & m_files
wxDirTraverseResult OnFile(const wxString &aFilePath) override
void deletePackageDirectories(const wxString &aPackageId, const std::forward_list< wxRegEx > &aKeep={})
Delete all package files.
void InstallFromFile(wxWindow *aParent, const wxString &aFilePath)
Installs package from an archive file on disk.
SYNC_QUEUE< PCM_TASK > m_install_queue
std::shared_ptr< PLUGIN_CONTENT_MANAGER > m_pcm
void Uninstall(const PCM_PACKAGE &aPackage)
Enqueue package uninstallation.
SYNC_QUEUE< PCM_TASK > m_download_queue
void DownloadAndInstall(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const bool isUpdate)
Enqueue package download and installation.
int downloadFile(const wxString &aFilePath, const wxString &aUrl)
Download URL to a file.
void installDownloadedPackage(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const wxFileName &aFilePath, const bool isUpdate)
Installs downloaded package archive.
bool extract(const wxString &aFilePath, const wxString &aPackageId, bool isMultiThreaded)
Extract package archive.
void RunQueue(wxWindow *aParent)
Run queue of pending actions.
std::mutex m_changed_package_types_guard
std::unique_ptr< DIALOG_PCM_PROGRESS > m_reporter
std::unordered_set< PCM_PACKAGE_TYPE > m_changed_package_types
bool pop(T &aReceiver)
Pop a value if the queue into the provided variable.
Definition: sync_queue.h:63
bool empty() const
Return true if the queue is empty.
Definition: sync_queue.h:82
size_t size() const
Return the size of the queue.
Definition: sync_queue.h:91
void push(T const &aValue)
Push a value onto the queue.
Definition: sync_queue.h:41
#define _(s)
nlohmann::json json
Definition: gerbview.cpp:44
std::function< int(size_t, size_t, size_t, size_t)> TRANSFER_CALLBACK
Wrapper interface around the curl_easy API/.
bool parse(std::istream &aStream, bool aVerbose)
Parse a PCB or footprint file from the given input stream.
const std::unordered_set< wxString > PCM_PACKAGE_DIRECTORIES({ "plugins", "footprints", "3dmodels", "symbols", "resources", "colors", })
< Contains list of all valid directories that get extracted from a package archive
void compile_keep_on_update_regex(const PCM_PACKAGE &pkg, const PACKAGE_VERSION &ver, std::forward_list< wxRegEx > &aKeepOnUpdate)
std::function< void()> PCM_TASK
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, const CPTREE &aTree)
Output a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:200
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_INFO
< Package version metadata Package metadata
Definition: pcm_data.h:74
std::vector< std::string > keep_on_update
Definition: pcm_data.h:85
Definition: pcm_data.h:138
Repository reference to a resource.
Definition: pcm_data.h:95
wxString identifier
Definition: pcm_data.h:99
std::vector< PACKAGE_VERSION > versions
Definition: pcm_data.h:107
PCM_PACKAGE_TYPE type
Definition: pcm_data.h:100
std::vector< std::string > keep_on_update
Definition: pcm_data.h:106
static bool CopyStreamData(wxInputStream &inputStream, wxOutputStream &outputStream, wxFileOffset size)