KiCad PCB EDA Suite
Loading...
Searching...
No Matches
downgrade_scan.h
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
14 * General 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#ifndef DOWNGRADE_SCAN_H
21#define DOWNGRADE_SCAN_H
22
23#include <cctype>
24#include <filesystem>
25#include <functional>
26#include <set>
27#include <string>
28#include <utility>
29#include <vector>
30
31#include <wx/dir.h>
32#include <wx/ffile.h>
33#include <wx/filefn.h>
34#include <wx/filename.h>
35#include <wx/intl.h>
36#include <wx/string.h>
37#include <wx/utils.h>
38
41{
42 const char* m_token;
44};
45
46
48inline std::string StripSexprStrings( const std::string& aText )
49{
50 std::string result;
51 bool inString = false;
52
53 for( size_t i = 0; i < aText.size(); i++ )
54 {
55 char c = aText[i];
56
57 // A quote is real only if an even number of backslashes precede it. This stops a value that
58 // ends in a backslash from swallowing the rest of the file.
59 if( c == '"' )
60 {
61 size_t backslashes = 0;
62
63 for( size_t k = i; k > 0 && aText[k - 1] == '\\'; k-- )
64 backslashes++;
65
66 if( backslashes % 2 == 0 )
67 {
68 inString = !inString;
69 continue;
70 }
71 }
72
73 if( !inString )
74 result += c;
75 }
76
77 return result;
78}
79
80
81inline bool ContainsSexprNode( const std::string& aText, const std::string& aToken )
82{
83 const std::string needle = "(" + aToken;
84
85 for( size_t pos = aText.find( needle ); pos != std::string::npos; pos = aText.find( needle, pos + 1 ) )
86 {
87 // Match the whole token, so "(net" is never mistaken for "(net_chain".
88 size_t after = pos + needle.size();
89 char next = ( after < aText.size() ) ? aText[after] : ' ';
90
91 if( !std::isalnum( static_cast<unsigned char>( next ) ) && next != '_' )
92 return true;
93 }
94
95 return false;
96}
97
98
101inline bool ContainsSexprValue( const std::string& aText, const std::string& aToken )
102{
103 for( size_t pos = aText.find( aToken ); pos != std::string::npos; pos = aText.find( aToken, pos + 1 ) )
104 {
105 char prev = pos > 0 ? aText[pos - 1] : ' ';
106 size_t after = pos + aToken.size();
107 char next = after < aText.size() ? aText[after] : ' ';
108
109 bool prevOk = !std::isalnum( static_cast<unsigned char>( prev ) ) && prev != '_';
110 bool nextOk = !std::isalnum( static_cast<unsigned char>( next ) ) && next != '_';
111
112 if( prevOk && nextOk )
113 return true;
114 }
115
116 return false;
117}
118
119
122inline wxString FindUnsupportedToken( const wxString& aSerialized, const DATED_TOKEN* aNodes, size_t aNodeCount,
123 const DATED_TOKEN* aValues, size_t aValueCount, int aTargetVersion )
124{
125 // Explicit UTF-8: the default locale conversion returns an empty string on unrepresentable
126 // characters, which would silently pass the scan.
127 std::string text = StripSexprStrings( aSerialized.ToStdString( wxConvUTF8 ) );
128
129 for( size_t i = 0; i < aNodeCount; i++ )
130 {
131 if( aTargetVersion < aNodes[i].m_introducedIn && ContainsSexprNode( text, aNodes[i].m_token ) )
132 return wxString::FromUTF8( aNodes[i].m_token );
133 }
134
135 for( size_t i = 0; i < aValueCount; i++ )
136 {
137 if( aTargetVersion < aValues[i].m_introducedIn && ContainsSexprValue( text, aValues[i].m_token ) )
138 return wxString::FromUTF8( aValues[i].m_token );
139 }
140
141 return wxEmptyString;
142}
143
144
146inline std::set<std::string> ExtractSexprNodeHeads( const std::string& aText )
147{
148 std::set<std::string> heads;
149
150 for( size_t i = 0; i + 1 < aText.size(); i++ )
151 {
152 if( aText[i] != '(' || !std::islower( static_cast<unsigned char>( aText[i + 1] ) ) )
153 continue;
154
155 size_t j = i + 1;
156
157 while( j < aText.size()
158 && ( std::islower( static_cast<unsigned char>( aText[j] ) )
159 || std::isdigit( static_cast<unsigned char>( aText[j] ) ) || aText[j] == '_' ) )
160 {
161 j++;
162 }
163
164 heads.insert( aText.substr( i + 1, j - i - 1 ) );
165 }
166
167 return heads;
168}
169
170
173inline bool IsDowngradeWorkingArtifact( const wxString& aPath )
174{
175 wxFileName fn( aPath );
176
177 if( fn.GetFullName().StartsWith( wxT( "_autosave-" ) ) )
178 return true;
179
180 for( const wxString& dir : fn.GetDirs() )
181 {
182 if( dir == wxT( ".history" ) || dir == wxT( ".git" ) || dir.EndsWith( wxT( "-backups" ) ) )
183 return true;
184 }
185
186 return false;
187}
188
189
193inline bool IsDowngradeDesignFile( const wxString& aPath )
194{
195 wxFileName fn( aPath );
196 wxString ext = fn.GetExt();
197
198 if( ext == wxT( "kicad_pro" ) || ext == wxT( "kicad_sch" ) || ext == wxT( "kicad_pcb" ) || ext == wxT( "kicad_dru" )
199 || ext == wxT( "kicad_wks" ) || ext == wxT( "kicad_sym" ) || ext == wxT( "kicad_mod" ) )
200 {
201 return true;
202 }
203
204 wxString name = fn.GetFullName();
205 return name == wxT( "sym-lib-table" ) || name == wxT( "fp-lib-table" );
206}
207
208
211inline bool VerifyDowngradedFile( const wxString& aPath, int aStampVersion,
212 const std::function<wxString( const wxString& )>& aFindForbidden,
213 wxString* aForbidden = nullptr )
214{
215 wxString content;
216 bool readOk;
217
218 {
219 wxFFile file( aPath, wxT( "rb" ) );
220 readOk = file.IsOpened() && file.ReadAll( &content );
221 }
222
223 if( !readOk )
224 return false;
225
226 if( !content.Contains( wxString::Format( wxT( "(version %d)" ), aStampVersion ) ) )
227 return false;
228
229 wxString forbidden = aFindForbidden( content );
230
231 if( aForbidden )
232 *aForbidden = forbidden;
233
234 return forbidden.IsEmpty();
235}
236
237
245
246
249{
250public:
254
256 {
257 for( const ENTRY& entry : m_entries )
258 {
259 removeOwnedFile( entry.m_temp );
260
261 if( !entry.m_keepBackup )
262 removeOwnedFile( entry.m_backup );
263 }
264 }
265
267 wxString Stage( const wxString& aDestination )
268 {
269 wxFileName destination( aDestination );
270
271 if( m_finished || destination.GetFullName().IsEmpty() || !destination.MakeAbsolute() || !destination.DirExists()
272 || wxDirExists( destination.GetFullPath() ) )
273 {
274 return wxEmptyString;
275 }
276
277 for( const ENTRY& entry : m_entries )
278 {
279 if( wxFileName( entry.m_destination ).SameAs( destination ) )
280 return wxEmptyString;
281 }
282
283 wxString temporary = wxFileName::CreateTempFileName( destination.GetFullPath() + wxT( ".downgrade_tmp." ) );
284
285 if( temporary.IsEmpty() )
286 return wxEmptyString;
287
288 ENTRY entry;
289 entry.m_temp = temporary;
290 entry.m_destination = destination.GetFullPath();
291 m_entries.push_back( std::move( entry ) );
292 return temporary;
293 }
294
296 void Discard( const wxString& aTemporary )
297 {
298 for( auto it = m_entries.begin(); it != m_entries.end(); ++it )
299 {
300 if( it->m_temp == aTemporary )
301 {
302 removeOwnedFile( it->m_temp );
303 m_entries.erase( it );
304 return;
305 }
306 }
307 }
308
311 wxString Commit()
312 {
313 if( m_finished )
314 return m_entries.empty() ? wxString() : m_entries.front().m_destination;
315
316 m_finished = true;
317
318 // Back up every existing destination before the first replacement, including symlinks.
319 for( ENTRY& entry : m_entries )
320 {
321 if( !wxFileExists( entry.m_temp ) || wxDirExists( entry.m_destination ) )
322 return entry.m_destination;
323
324 std::error_code error;
325 auto destination = nativePath( entry.m_destination );
326 bool symlink = std::filesystem::is_symlink( destination, error );
327
328 if( error && error != std::errc::no_such_file_or_directory )
329 return entry.m_destination;
330
331 entry.m_hadOriginal = symlink || wxFileExists( entry.m_destination );
332
333 if( !entry.m_hadOriginal )
334 continue;
335
336 if( wxFileExists( entry.m_destination ) )
337 {
338 auto permissions = std::filesystem::status( destination, error ).permissions();
339
340 if( error )
341 return entry.m_destination;
342
343 std::filesystem::permissions( nativePath( entry.m_temp ), permissions, error );
344
345 if( error )
346 return entry.m_destination;
347 }
348
349 entry.m_backup = wxFileName::CreateTempFileName( entry.m_destination + wxT( ".downgrade_backup." ) );
350
351 if( entry.m_backup.IsEmpty() )
352 return entry.m_destination;
353
354 if( symlink )
355 {
356 // copy_symlink creates the backup exclusively and preserves relative link targets.
357 if( !wxRemoveFile( entry.m_backup ) )
358 return entry.m_destination;
359
360 wxString backup = entry.m_backup;
361 entry.m_backup.clear();
362 std::filesystem::copy_symlink( destination, nativePath( backup ), error );
363
364 if( error )
365 return entry.m_destination;
366
367 entry.m_backup = backup;
368 }
369 else if( !wxCopyFile( entry.m_destination, entry.m_backup, true ) )
370 {
371 return entry.m_destination;
372 }
373 }
374
375 for( ENTRY& entry : m_entries )
376 {
377 // An overwrite may disturb its destination even when the rename reports a failure.
378 entry.m_attemptedReplacement = true;
379
380 if( !wxRenameFile( entry.m_temp, entry.m_destination, true ) )
381 {
382 rollback();
383 return entry.m_destination;
384 }
385
386 entry.m_temp.clear();
387 }
388
389 return wxEmptyString;
390 }
391
393 const wxString& GetRecoveryError() const { return m_recoveryError; }
394
395private:
396 struct ENTRY
397 {
398 wxString m_temp;
400 wxString m_backup;
401 bool m_hadOriginal = false;
403 bool m_keepBackup = false;
404 };
405
406 static void removeOwnedFile( const wxString& aPath )
407 {
408 if( ownedFileExists( aPath ) )
409 wxRemoveFile( aPath );
410 }
411
412 static bool ownedFileExists( const wxString& aPath )
413 {
414 if( aPath.IsEmpty() )
415 return false;
416
417 std::error_code error;
418 return wxFileExists( aPath ) || std::filesystem::is_symlink( nativePath( aPath ), error );
419 }
420
421 static std::filesystem::path nativePath( const wxString& aPath )
422 {
423#ifdef _WIN32
424 return std::filesystem::path( aPath.ToStdWstring() );
425#else
426 return std::filesystem::path( aPath.utf8_string() );
427#endif
428 }
429
430 void rollback()
431 {
432 for( auto it = m_entries.rbegin(); it != m_entries.rend(); ++it )
433 {
434 ENTRY& entry = *it;
435
436 if( !entry.m_attemptedReplacement )
437 continue;
438
439 if( entry.m_hadOriginal )
440 {
441 if( wxRenameFile( entry.m_backup, entry.m_destination, true ) )
442 {
443 entry.m_backup.clear();
444 }
445 else
446 {
447 entry.m_keepBackup = true;
448 m_recoveryError += wxString::Format( _( "Could not restore '%s'. The original is saved at '%s'." ),
449 entry.m_destination, entry.m_backup )
450 + wxT( "\n" );
451 }
452 }
453 else if( ownedFileExists( entry.m_destination ) && !wxRemoveFile( entry.m_destination ) )
454 {
456 wxString::Format( _( "Could not remove incomplete output '%s'." ), entry.m_destination )
457 + wxT( "\n" );
458 }
459 }
460 }
461
462 std::vector<ENTRY> m_entries;
463 bool m_finished = false;
465};
466
467
472inline wxString
473DowngradeLibraryFilesInPlace( const wxString& aDir, const wxString& aGlob, int aStampVersion,
474 const std::function<wxString( const wxString& )>& aFindForbidden,
475 const std::function<DOWNGRADE_FILE_RESULT( const wxString&, const wxString& )>& aConvert,
476 DOWNGRADE_FILE_TRANSACTION* aTransaction = nullptr, wxString* aRecoveryError = nullptr )
477{
478 if( aRecoveryError )
479 aRecoveryError->clear();
480
481 wxArrayString files;
482 wxDir::GetAllFiles( aDir, &files, aGlob );
483
484 DOWNGRADE_FILE_TRANSACTION localTransaction;
485 DOWNGRADE_FILE_TRANSACTION& transaction = aTransaction ? *aTransaction : localTransaction;
486
487 for( const wxString& file : files )
488 {
489 if( IsDowngradeWorkingArtifact( file ) )
490 continue;
491
492 wxString temporary = transaction.Stage( file );
493
494 if( temporary.IsEmpty() )
495 return file;
496
497 switch( aConvert( file, temporary ) )
498 {
499 case DOWNGRADE_FILE_RESULT::SKIPPED: transaction.Discard( temporary ); continue;
500
501 case DOWNGRADE_FILE_RESULT::REFUSED: return file;
502
504 }
505
506 if( !VerifyDowngradedFile( temporary, aStampVersion, aFindForbidden ) )
507 return file;
508 }
509
510 if( aTransaction )
511 return wxEmptyString;
512
513 wxString failed = transaction.Commit();
514
515 if( aRecoveryError )
516 *aRecoveryError = transaction.GetRecoveryError();
517
518 return failed;
519}
520
521#endif // DOWNGRADE_SCAN_H
const char * name
Stage related design and library files together, retaining originals until every write is ready.
static std::filesystem::path nativePath(const wxString &aPath)
static void removeOwnedFile(const wxString &aPath)
const wxString & GetRecoveryError() const
Recovery details are nonempty only if restoring an original or removing a new output failed.
std::vector< ENTRY > m_entries
DOWNGRADE_FILE_TRANSACTION(const DOWNGRADE_FILE_TRANSACTION &)=delete
DOWNGRADE_FILE_TRANSACTION()=default
wxString Commit()
Replace all destinations, rolling back earlier replacements if a rename fails.
wxString Stage(const wxString &aDestination)
Return an exclusively-created sibling staging file, or empty if it cannot be created.
DOWNGRADE_FILE_TRANSACTION & operator=(const DOWNGRADE_FILE_TRANSACTION &)=delete
static bool ownedFileExists(const wxString &aPath)
void Discard(const wxString &aTemporary)
Forget a skipped conversion without exposing its empty staging file as an output.
wxString DowngradeLibraryFilesInPlace(const wxString &aDir, const wxString &aGlob, int aStampVersion, const std::function< wxString(const wxString &)> &aFindForbidden, const std::function< DOWNGRADE_FILE_RESULT(const wxString &, const wxString &)> &aConvert, DOWNGRADE_FILE_TRANSACTION *aTransaction=nullptr, wxString *aRecoveryError=nullptr)
Downgrade every library file matching aGlob under aDir in place.
bool IsDowngradeWorkingArtifact(const wxString &aPath)
Editor working state (autosaves, local history, backups) holds current-format copies nothing downgrad...
bool IsDowngradeDesignFile(const wxString &aPath)
Files the project export carries over.
wxString FindUnsupportedToken(const wxString &aSerialized, const DATED_TOKEN *aNodes, size_t aNodeCount, const DATED_TOKEN *aValues, size_t aValueCount, int aTargetVersion)
Scan serialized s-expression text against dated node and value denylists.
std::string StripSexprStrings(const std::string &aText)
Drop quoted strings so a token inside a text value cannot cause a false refusal.
bool ContainsSexprValue(const std::string &aText, const std::string &aToken)
Some features are written as a bareword value, not a node head, so the node scan cannot see them.
std::set< std::string > ExtractSexprNodeHeads(const std::string &aText)
Every lowercase node head in the text, e.g. for checking output against a release keyword set.
bool ContainsSexprNode(const std::string &aText, const std::string &aToken)
DOWNGRADE_FILE_RESULT
What a per-file converter did with one library file.
@ SKIPPED
Not a convertible file, leave it alone.
@ REFUSED
The target cannot represent this file.
@ CONVERTED
The temp file holds the downgraded copy.
bool VerifyDowngradedFile(const wxString &aPath, int aStampVersion, const std::function< wxString(const wxString &)> &aFindForbidden, wxString *aForbidden=nullptr)
Read back a downgraded output and require the target stamp plus a clean token scan.
#define _(s)
CITER next(CITER it)
Definition ptree.cpp:120
A token dated by the format that introduced it, for the fail-closed downgrade gates.
const char * m_token
wxString result
Test unit parsing edge cases and error handling.