KiCad PCB EDA Suite
Loading...
Searching...
No Matches
windows/io.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
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 <https://www.gnu.org/licenses/>.
18*/
19
20#include <kiplatform/io.h>
21
22#include <wx/string.h>
23#include <wx/wxcrt.h>
24#include <wx/filename.h>
25
26#include <cstdio>
27#include <io.h>
28#include <stdexcept>
29#include <string>
30#include <vector>
31#include <windows.h>
32#include <shlwapi.h>
33#include <winternl.h>
34
35// NtQueryDirectoryFile-based directory enumeration for fast file listing.
36// This approach is based on git-for-windows fscache implementation:
37// https://github.com/git-for-windows/git/blob/main/compat/win32/fscache.c
38// Copyright (C) Johannes Schindelin and the Git for Windows project
39// Licensed under GPL v2.
40//
41// FILE_FULL_DIR_INFORMATION is documented in the Windows Driver Kit but not the SDK.
42
43#if !defined( __MINGW32__ ) // already defined in the included mingw header <winternl.h>
44 // So do not redefine it on mingw
60#endif
61
62typedef NTSTATUS( NTAPI* PFN_NtQueryDirectoryFile )( HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID,
66
67#define FileFullDirectoryInformation ( (FILE_INFORMATION_CLASS) 2 )
68
69// Define USE_MSYS2_FALlBACK if the code for _MSC_VER does not compile on msys2
70//#define USE_MSYS2_FALLBACK
71
72FILE* KIPLATFORM::IO::SeqFOpen( const wxString& aPath, const wxString& aMode )
73{
74#if defined( _MSC_VER ) || !defined( USE_MSYS2_FALLBACK )
75 // We need to use the win32 api to setup a file handle with sequential scan flagged
76 // and pass it up the chain to create a normal FILE stream
77 HANDLE hFile = INVALID_HANDLE_VALUE;
78 hFile = CreateFileW( aPath.wc_str(),
79 GENERIC_READ,
80 FILE_SHARE_READ,
81 NULL,
82 OPEN_EXISTING,
83 FILE_FLAG_SEQUENTIAL_SCAN,
84 NULL );
85
86 if (hFile == INVALID_HANDLE_VALUE)
87 {
88 return NULL;
89 }
90
91 int fd = _open_osfhandle( reinterpret_cast<intptr_t>( hFile ), 0 );
92
93 if( fd == -1 )
94 {
95 // close the handle manually as the ownership didnt transfer
96 CloseHandle( hFile );
97 return NULL;
98 }
99
100 FILE* fp = _fdopen( fd, aMode.c_str() );
101
102 if( !fp )
103 {
104 // close the file descriptor manually as the ownership didnt transfer
105 _close( fd );
106 }
107
108 return fp;
109#else
110 // Fallback for MSYS2
111 return wxFopen( aPath, aMode );
112#endif
113}
114
115bool KIPLATFORM::IO::DuplicatePermissions( const wxString &aSrc, const wxString &aDest )
116{
117 // Only copy the DACL. Copying OWNER/GROUP would require SE_RESTORE_NAME when the
118 // target is owned by a different principal (common for files under ProgramData or
119 // on network shares), turning ACL preservation into a hard failure. The temp file
120 // is created by the current user, so leaving owner as the current user is correct.
121 const SECURITY_INFORMATION secInfo = DACL_SECURITY_INFORMATION;
122
123 // Size-probe call: required buffer size is returned via dwSize. By API contract this
124 // call fails with ERROR_INSUFFICIENT_BUFFER; the previous implementation wrapped it
125 // in an if() and therefore never executed the body, silently returning false on every
126 // save and surfacing as "Cannot copy permissions" once atomic save made the failure
127 // fatal.
128 DWORD dwSize = 0;
129 GetFileSecurityW( aSrc.wc_str(), secInfo, nullptr, 0, &dwSize );
130
131 if( dwSize == 0 || GetLastError() != ERROR_INSUFFICIENT_BUFFER )
132 return false;
133
134 std::vector<BYTE> sdBuffer( dwSize );
135 PSECURITY_DESCRIPTOR pSD = static_cast<PSECURITY_DESCRIPTOR>( sdBuffer.data() );
136
137 return GetFileSecurityW( aSrc.wc_str(), secInfo, pSD, dwSize, &dwSize )
138 && SetFileSecurityW( aDest.wc_str(), secInfo, pSD );
139}
140
141bool KIPLATFORM::IO::MakeWriteable( const wxString& aFilePath )
142{
143 DWORD attrs = GetFileAttributesW( aFilePath.wc_str() );
144
145 if( attrs == INVALID_FILE_ATTRIBUTES )
146 return false;
147
148 // Remove read-only and hidden attributes if present. Both of these can prevent file
149 // operations on Windows. Hidden files in particular can cause issues when files are
150 // synced via cloud services like OneDrive.
151 DWORD attrsToRemove = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN;
152
153 if( attrs & attrsToRemove )
154 {
155 attrs &= ~attrsToRemove;
156 return SetFileAttributesW( aFilePath.wc_str(), attrs ) != 0;
157 }
158
159 return true;
160}
161
163{
164 TARGET_ATTRS snapshot;
165 DWORD attrs = GetFileAttributesW( aPath.wc_str() );
166
167 if( attrs == INVALID_FILE_ATTRIBUTES )
168 return snapshot;
169
170 // Only preserve bits that SetFileSecurity (used by DuplicatePermissions) does not
171 // carry across a rename. Other attributes on the new file come from the temp's
172 // default creation attrs and should not be overwritten here.
173 snapshot.value = static_cast<std::uint32_t>( attrs )
174 & ( FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN );
175 snapshot.captured = true;
176 return snapshot;
177}
178
179
180bool KIPLATFORM::IO::ApplyTargetAttributes( const wxString& aPath, const TARGET_ATTRS& aAttrs )
181{
182 if( !aAttrs.captured )
183 return true;
184
185 DWORD current = GetFileAttributesW( aPath.wc_str() );
186
187 if( current == INVALID_FILE_ATTRIBUTES )
188 return false;
189
190 DWORD merged = current | static_cast<DWORD>( aAttrs.value );
191
192 if( merged == current )
193 return true;
194
195 return SetFileAttributesW( aPath.wc_str(), merged ) != 0;
196}
197
198
199FILE* KIPLATFORM::IO::OpenUniqueSiblingTempFile( const wxString& aTargetPath,
200 const wxString& aMode, wxString* aTempPathOut,
201 wxString* aError )
202{
203 // Exclusive-create closes the TOCTOU window: if another process pre-created a file
204 // at the candidate path, CreateFileW with CREATE_NEW fails and we retry.
205 for( unsigned attempt = 0; attempt < 32; ++attempt )
206 {
207 wxString candidate = MakeSiblingTempPath( aTargetPath );
208 HANDLE h = CreateFileW( candidate.wc_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
209 FILE_ATTRIBUTE_NORMAL, nullptr );
210
211 if( h != INVALID_HANDLE_VALUE )
212 {
213 int fd = _open_osfhandle( reinterpret_cast<intptr_t>( h ), _O_WRONLY | _O_BINARY );
214
215 if( fd < 0 )
216 {
217 CloseHandle( h );
218 DeleteFileW( candidate.wc_str() );
219
220 if( aError )
221 {
222 *aError = wxString::Format( wxT( "_open_osfhandle failed for '%s'" ),
223 candidate );
224 }
225
226 return nullptr;
227 }
228
229 FILE* fp = _wfdopen( fd, aMode.wc_str() );
230
231 if( !fp )
232 {
233 _close( fd ); // also closes the HANDLE
234 DeleteFileW( candidate.wc_str() );
235
236 if( aError )
237 *aError = wxString::Format( wxT( "_wfdopen failed for '%s'" ), candidate );
238
239 return nullptr;
240 }
241
242 if( aTempPathOut )
243 *aTempPathOut = candidate;
244
245 return fp;
246 }
247
248 DWORD err = GetLastError();
249
250 if( err != ERROR_FILE_EXISTS && err != ERROR_ALREADY_EXISTS )
251 {
252 if( aError )
253 *aError = wxString::Format( wxT( "CreateFile failed for '%s' (Win32 %lu)" ),
254 candidate, err );
255
256 return nullptr;
257 }
258 }
259
260 if( aError )
261 *aError = wxT( "Exhausted temp-file retry budget" );
262
263 return nullptr;
264}
265
266
267wxString KIPLATFORM::IO::ResolveSymlinkTarget( const wxString& aPath )
268{
269 // Windows reparse points are semantically richer than POSIX symlinks (junctions,
270 // mount points, symlinks). The pre-atomic save code used wxFopen which opened
271 // through symlinks; MoveFileExW with MOVEFILE_REPLACE_EXISTING also follows
272 // reparse points for the target, so no pre-resolution is needed here.
273 return aPath;
274}
275
276
277bool KIPLATFORM::IO::IsFileHidden( const wxString& aFileName )
278{
279 bool result = false;
280
281 if( ( GetFileAttributesW( aFileName.fn_str() ) & FILE_ATTRIBUTE_HIDDEN ) )
282 result = true;
283
284 return result;
285}
286
287
288void KIPLATFORM::IO::LongPathAdjustment( wxFileName& aFilename )
289{
290 // dont shortcut this for shorter lengths as there are uses like directory
291 // paths that exceed the path length when you start traversing their subdirectories
292 // so we want to start with the long path prefix all the time
293
294 if( aFilename.GetVolume().Length() == 1 )
295 // assume single letter == drive volume
296 aFilename.SetVolume( "\\\\?\\" + aFilename.GetVolume() + ":" );
297 else if( aFilename.GetVolume().Length() > 1
298 && aFilename.GetVolume().StartsWith( wxT( "\\\\" ) )
299 && !aFilename.GetVolume().StartsWith( wxT( "\\\\?" ) ) )
300 // unc path aka network share, wx returns with \\ already
301 // so skip the first slash and combine with the prefix
302 // which in the case of UNCs is actually \\?\UNC<server><share>
303 // where UNC is literally the text UNC
304 aFilename.SetVolume( "\\\\?\\UNC" + aFilename.GetVolume().Mid( 1 ) );
305 else if( aFilename.GetVolume().StartsWith( wxT( "\\\\?" ) )
306 && aFilename.GetDirs().size() >= 2
307 && aFilename.GetDirs()[0] == "UNC" )
308 {
309 // wxWidgets can parse \\?\UNC<server> into a mess
310 // UNC gets stored into a directory
311 // volume gets reduced to just \\?
312 // so we need to repair it
313 aFilename.SetVolume( "\\\\?\\UNC\\" + aFilename.GetDirs()[1] );
314 aFilename.RemoveDir( 0 );
315 aFilename.RemoveDir( 0 );
316 }
317}
318
319
320long long KIPLATFORM::IO::TimestampDir( const wxString& aDirPath, const wxString& aFilespec )
321{
322 long long timestamp = 0;
323
324 // Use NtQueryDirectoryFile for fast directory enumeration (same approach as git-for-windows).
325 // This retrieves multiple directory entries per syscall into a large buffer, reducing
326 // kernel transitions compared to FindFirstFile/FindNextFile.
327 static PFN_NtQueryDirectoryFile pNtQueryDirectoryFile = nullptr;
328
329 if( !pNtQueryDirectoryFile )
330 {
331 HMODULE ntdll = GetModuleHandleW( L"ntdll.dll" );
332
333 if( ntdll )
334 {
335 pNtQueryDirectoryFile =
336 (PFN_NtQueryDirectoryFile) GetProcAddress( ntdll, "NtQueryDirectoryFile" );
337 }
338 }
339
340 if( !pNtQueryDirectoryFile )
341 return timestamp;
342
343 std::wstring dirPath( aDirPath.t_str() );
344
345 if( !dirPath.empty() && dirPath.back() != L'\\' )
346 dirPath += L'\\';
347
348 // Prefix with \\?\ for long path support, handling UNC paths specially
349 std::wstring ntPath;
350
351 if( dirPath.size() >= 2 && dirPath[0] == L'\\' && dirPath[1] == L'\\' )
352 {
353 if( dirPath.size() >= 4 && dirPath[2] == L'?' && dirPath[3] == L'\\' )
354 {
355 // Already has \\?\ prefix
356 ntPath = dirPath;
357 }
358 else
359 {
360 // UNC path: \\server\share -> \\?\UNC\server\share
361 ntPath = L"\\\\?\\UNC\\" + dirPath.substr( 2 );
362 }
363 }
364 else
365 {
366 // Local path: C:\foo -> \\?\C:\foo
367 ntPath = L"\\\\?\\" + dirPath;
368 }
369
370 HANDLE hDir = CreateFileW( ntPath.c_str(), FILE_LIST_DIRECTORY,
371 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
372 OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr );
373
374 if( hDir == INVALID_HANDLE_VALUE )
375 return timestamp;
376
377 std::wstring pattern( aFilespec.t_str() );
378
379 // 64KB buffer for directory entries (same size as git-for-windows)
380 alignas( sizeof( LONGLONG ) ) char buffer[64 * 1024];
381
382 IO_STATUS_BLOCK iosb;
383 NTSTATUS status;
384 bool firstQuery = true;
385
386 for( ;; )
387 {
388 status = pNtQueryDirectoryFile( hDir, nullptr, nullptr, nullptr, &iosb, buffer,
389 sizeof( buffer ), FileFullDirectoryInformation, FALSE,
390 nullptr, firstQuery ? TRUE : FALSE );
391 firstQuery = false;
392
393 if( status != 0 )
394 break;
395
397
398 for( ;; )
399 {
400 // Extract null-terminated filename
401 std::wstring fileName( dirInfo->FileName, dirInfo->FileNameLength / sizeof( WCHAR ) );
402
403 // Skip directories and match against pattern
404 if( !( dirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY )
405 && PathMatchSpecW( fileName.c_str(), pattern.c_str() ) )
406 {
407 // Shift right by 13 (~0.8ms resolution) to avoid overflow when summing many files
408 timestamp += dirInfo->LastWriteTime.QuadPart >> 13;
409 timestamp += dirInfo->EndOfFile.LowPart;
410 }
411
412 if( dirInfo->NextEntryOffset == 0 )
413 break;
414
415 dirInfo = (PFILE_FULL_DIR_INFORMATION) ( (char*) dirInfo + dirInfo->NextEntryOffset );
416 }
417 }
418
419 CloseHandle( hDir );
420
421 return timestamp;
422}
423
424
425bool KIPLATFORM::IO::FlushToDisk( FILE* aFp )
426{
427 if( !aFp )
428 return false;
429
430 if( std::fflush( aFp ) != 0 )
431 return false;
432
433 int fd = _fileno( aFp );
434
435 if( fd < 0 )
436 return false;
437
438 HANDLE h = reinterpret_cast<HANDLE>( _get_osfhandle( fd ) );
439
440 if( h == INVALID_HANDLE_VALUE )
441 return false;
442
443 return FlushFileBuffers( h ) != 0;
444}
445
446
447bool KIPLATFORM::IO::FlushDirectory( const wxString& aDirPath )
448{
449 // NTFS metadata journaling commits rename operations durably on its own, so there is
450 // no equivalent of POSIX dir-fsync. Report success unconditionally.
451 (void) aDirPath;
452 return true;
453}
454
455
456bool KIPLATFORM::IO::AtomicRename( const wxString& aSrc, const wxString& aDst, wxString* aError )
457{
458 // Try MoveFileEx first. MOVEFILE_WRITE_THROUGH ensures the rename is committed before
459 // return, so a power loss after success does not lose the replacement. MOVEFILE_REPLACE_EXISTING
460 // allows overwriting the destination (the caller has already verified this is the intent).
461 // A brief retry loop absorbs transient antivirus / indexer / cloud-sync locks that would
462 // otherwise surface as ERROR_SHARING_VIOLATION or ERROR_ACCESS_DENIED.
463 DWORD lastError = 0;
464
465 for( int attempt = 0; attempt < 10; ++attempt )
466 {
467 if( MoveFileExW( aSrc.wc_str(), aDst.wc_str(),
468 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH ) )
469 return true;
470
471 lastError = GetLastError();
472
473 if( lastError != ERROR_SHARING_VIOLATION && lastError != ERROR_ACCESS_DENIED
474 && lastError != ERROR_LOCK_VIOLATION )
475 break;
476
477 Sleep( 50 );
478 }
479
480 // Fall back to ReplaceFileW, which handles some share-mode cases MoveFileEx cannot
481 // (for instance when the destination is open for reading with FILE_SHARE_DELETE).
482 if( ReplaceFileW( aDst.wc_str(), aSrc.wc_str(), nullptr, REPLACEFILE_WRITE_THROUGH, nullptr,
483 nullptr ) )
484 return true;
485
486 DWORD fallbackError = GetLastError();
487
488 if( aError )
489 {
490 wchar_t* msg = nullptr;
491 FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
492 | FORMAT_MESSAGE_IGNORE_INSERTS,
493 nullptr, fallbackError ? fallbackError : lastError,
494 MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), reinterpret_cast<LPWSTR>( &msg ),
495 0, nullptr );
496
497 if( msg )
498 {
499 *aError = wxString( msg );
500 LocalFree( msg );
501 }
502 else
503 {
504 *aError = wxString::Format( wxT( "Win32 error %lu" ), fallbackError ? fallbackError
505 : lastError );
506 }
507 }
508
509 return false;
510}
511
512
513KIPLATFORM::IO::MAPPED_FILE::MAPPED_FILE( const wxString& aFileName )
514{
515 m_fileHandle = CreateFileW( aFileName.wc_str(), GENERIC_READ, FILE_SHARE_READ,
516 nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr );
517
518 if( m_fileHandle == INVALID_HANDLE_VALUE )
519 {
520 m_fileHandle = nullptr;
521 throw std::runtime_error( std::string( "Cannot open file: " )
522 + aFileName.ToStdString() );
523 }
524
525 LARGE_INTEGER fileSize;
526
527 if( !GetFileSizeEx( m_fileHandle, &fileSize ) )
528 {
529 CloseHandle( m_fileHandle );
530 m_fileHandle = nullptr;
531 throw std::runtime_error( std::string( "Cannot determine file size: " )
532 + aFileName.ToStdString() );
533 }
534
535 m_size = static_cast<size_t>( fileSize.QuadPart );
536
537 if( m_size == 0 )
538 {
539 CloseHandle( m_fileHandle );
540 m_fileHandle = nullptr;
541 return;
542 }
543
544 m_mapHandle = CreateFileMappingW( m_fileHandle, nullptr, PAGE_READONLY, 0, 0, nullptr );
545
546 if( !m_mapHandle )
547 {
548 CloseHandle( m_fileHandle );
549 m_fileHandle = nullptr;
550 readIntoBuffer( aFileName );
551 return;
552 }
553
554 void* ptr = MapViewOfFile( m_mapHandle, FILE_MAP_READ, 0, 0, 0 );
555
556 if( !ptr )
557 {
558 CloseHandle( m_mapHandle );
559 m_mapHandle = nullptr;
560 CloseHandle( m_fileHandle );
561 m_fileHandle = nullptr;
562 readIntoBuffer( aFileName );
563 return;
564 }
565
566 m_data = static_cast<const uint8_t*>( ptr );
567}
568
569
571{
572 if( m_data && m_mapHandle )
573 UnmapViewOfFile( m_data );
574
575 if( m_mapHandle )
576 CloseHandle( m_mapHandle );
577
578 if( m_fileHandle )
579 CloseHandle( m_fileHandle );
580}
MAPPED_FILE(const wxString &aFileName)
Definition unix/io.cpp:199
void readIntoBuffer(const wxString &aFileName)
const uint8_t * m_data
Definition io.h:56
wxString MakeSiblingTempPath(const wxString &aTargetPath)
Returns a unique sibling path of aTargetPath suitable as an atomic-save temp file.
Definition common/io.cpp:45
bool FlushDirectory(const wxString &aDirPath)
Forces a directory entry's metadata to stable storage.
void LongPathAdjustment(wxFileName &aFilename)
Adjusts a filename to be a long path compatible.
Definition unix/io.cpp:117
FILE * SeqFOpen(const wxString &aPath, const wxString &mode)
Opens the file like fopen but sets flags (if available) for sequential read hinting.
Definition unix/io.cpp:39
TARGET_ATTRS CaptureTargetAttributes(const wxString &aPath)
Captures attributes of an existing aPath that must survive an atomic rename.
Definition unix/io.cpp:94
bool DuplicatePermissions(const wxString &aSrc, const wxString &aDest)
Duplicates the file security data from one file to another ensuring that they are the same between bo...
Definition unix/io.cpp:55
wxString ResolveSymlinkTarget(const wxString &aPath)
If aPath is a symlink on POSIX, returns the canonical path of its referent so atomic-save operations ...
bool IsFileHidden(const wxString &aFileName)
Helper function to determine the status of the 'Hidden' file attribute.
Definition unix/io.cpp:109
bool AtomicRename(const wxString &aSrc, const wxString &aDst, wxString *aError=nullptr)
Atomically replaces aDst with aSrc.
FILE * OpenUniqueSiblingTempFile(const wxString &aTargetPath, const wxString &aMode, wxString *aTempPathOut, wxString *aError=nullptr)
Opens a fresh sibling temp file next to aTargetPath with exclusive-create semantics (POSIX O_CREAT|O_...
Definition common/io.cpp:66
bool MakeWriteable(const wxString &aFilePath)
Ensures that a file has write permissions.
Definition unix/io.cpp:78
bool ApplyTargetAttributes(const wxString &aPath, const TARGET_ATTRS &aAttrs)
Re-applies attributes previously captured by CaptureTargetAttributes.
Definition unix/io.cpp:103
long long TimestampDir(const wxString &aDirPath, const wxString &aFilespec)
Computes a hash of modification times and sizes for files matching a pattern.
Definition unix/io.cpp:123
bool FlushToDisk(FILE *aFp)
Flushes user-space buffers for aFp and forces the kernel/filesystem to commit the file's data blocks ...
Definition unix/io.cpp:182
Opaque snapshot of filesystem attributes that MakeWriteable may alter and that the atomic rename sequ...
Definition io.h:209
wxString result
Test unit parsing edge cases and error handling.
typedef PUNICODE_STRING
typedef FILE_INFORMATION_CLASS
typedef BOOLEAN
struct _FILE_FULL_DIR_INFORMATION FILE_FULL_DIR_INFORMATION
typedef NTSTATUS(NTAPI *PFN_NtQueryDirectoryFile)(HANDLE
typedef PIO_STATUS_BLOCK
typedef PIO_APC_ROUTINE
typedef HANDLE
typedef PVOID
struct _FILE_FULL_DIR_INFORMATION * PFILE_FULL_DIR_INFORMATION
typedef ULONG
#define FileFullDirectoryInformation