KiCad PCB EDA Suite
Loading...
Searching...
No Matches
common/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/crt.h>
23#include <wx/filename.h>
24#include <wx/log.h>
25#include <wx/string.h>
26
27#include <atomic>
28#include <cstdio>
29#include <stdexcept>
30#include <string>
31
32#if defined( _WIN32 )
33#include <process.h>
34#else
35#include <cerrno>
36#include <climits>
37#include <cstdlib>
38#include <cstring>
39#include <fcntl.h>
40#include <sys/stat.h>
41#include <unistd.h>
42#endif
43
44
45wxString KIPLATFORM::IO::MakeSiblingTempPath( const wxString& aTargetPath )
46{
47 // Keeping the temp file on the same filesystem as the final target is required:
48 // rename() across filesystems is not atomic, and MoveFileEx on Windows will fail or
49 // fall back to copy+delete across volumes.
50 static std::atomic<unsigned> s_counter{ 0 };
51
52 unsigned counter = s_counter.fetch_add( 1, std::memory_order_relaxed );
53
54#if defined( _WIN32 )
55 unsigned pid = static_cast<unsigned>( _getpid() );
56#else
57 unsigned pid = static_cast<unsigned>( getpid() );
58#endif
59
60 return aTargetPath + wxString::Format( wxT( ".kicad-save-%u-%u" ), pid, counter );
61}
62
63
64#if !defined( _WIN32 )
65
66FILE* KIPLATFORM::IO::OpenUniqueSiblingTempFile( const wxString& aTargetPath, const wxString& aMode,
67 wxString* aTempPathOut, wxString* aError )
68{
69 // Exclusive-create closes the TOCTOU window: if another process pre-created a file
70 // at the candidate path, O_EXCL fails and we retry with a new counter value.
71 for( unsigned attempt = 0; attempt < 32; ++attempt )
72 {
73 wxString candidate = MakeSiblingTempPath( aTargetPath );
74 int fd = open( candidate.fn_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600 );
75
76 if( fd >= 0 )
77 {
78 FILE* fp = fdopen( fd, aMode.mb_str() );
79
80 if( !fp )
81 {
82 int err = errno;
83 close( fd );
84 unlink( candidate.fn_str() );
85
86 if( aError )
87 {
88 *aError = wxString::Format( wxT( "fdopen failed for temp file '%s': %s" ),
89 candidate, wxString::FromUTF8( strerror( err ) ) );
90 }
91
92 return nullptr;
93 }
94
95 if( aTempPathOut )
96 *aTempPathOut = candidate;
97
98 return fp;
99 }
100
101 if( errno != EEXIST )
102 {
103 if( aError )
104 {
105 *aError = wxString::Format( wxT( "Cannot create temp file '%s': %s" ), candidate,
106 wxString::FromUTF8( strerror( errno ) ) );
107 }
108
109 return nullptr;
110 }
111 }
112
113 if( aError )
114 *aError = wxT( "Exhausted temp-file retry budget" );
115
116 return nullptr;
117}
118
119
120wxString KIPLATFORM::IO::ResolveSymlinkTarget( const wxString& aPath )
121{
122 // Users commonly symlink shared config/project files into their working directory.
123 // Atomic rename would replace the symlink with a regular file; resolve so the save
124 // lands on the referent instead.
125 struct stat st;
126
127 if( lstat( aPath.fn_str(), &st ) != 0 || !S_ISLNK( st.st_mode ) )
128 return aPath;
129
130 char resolved[PATH_MAX];
131
132 if( realpath( aPath.fn_str(), resolved ) )
133 return wxString::FromUTF8( resolved );
134
135 return aPath;
136}
137
138
139bool KIPLATFORM::IO::FlushDirectory( const wxString& aDirPath )
140{
141 int fd = open( aDirPath.fn_str(), O_RDONLY
142#if defined( O_DIRECTORY )
143 | O_DIRECTORY
144#endif
145 );
146
147 if( fd < 0 )
148 {
149 // NFS and some FUSE mounts reject O_DIRECTORY or reject fsync on directories;
150 // treat those as non-fatal since the file's own fsync is what matters most.
151 return errno == EINVAL || errno == ENOTSUP;
152 }
153
154 int rc = fsync( fd );
155 int err = errno;
156 close( fd );
157
158 return rc == 0 || err == EINVAL;
159}
160
161
162bool KIPLATFORM::IO::AtomicRename( const wxString& aSrc, const wxString& aDst, wxString* aError )
163{
164 if( rename( aSrc.fn_str(), aDst.fn_str() ) == 0 )
165 return true;
166
167 if( aError )
168 *aError = wxString::FromUTF8( strerror( errno ) );
169
170 return false;
171}
172
173#endif // !_WIN32
174
175
176bool KIPLATFORM::IO::CommitTempFile( const wxString& aTempPath, const wxString& aTargetPath,
177 wxString* aError )
178{
179 TARGET_ATTRS snapshot;
180 const bool targetExists = wxFileName::FileExists( aTargetPath );
181
182 if( targetExists )
183 {
184 // Snapshot first so we can re-apply after rename. DuplicatePermissions must
185 // read target's original mode (POSIX) before any mutation, so it follows the
186 // snapshot and precedes MakeWriteable.
187 snapshot = CaptureTargetAttributes( aTargetPath );
188
189 if( !DuplicatePermissions( aTargetPath, aTempPath ) )
190 {
191 // Failing here means the new file would land with creation-default
192 // permissions instead of the target's. Bail out while the rename hasn't
193 // happened yet so the user's original file is still untouched.
194 if( aError )
195 {
196 *aError = wxString::Format( wxT( "Cannot copy permissions from '%s' to '%s'" ),
197 aTargetPath, aTempPath );
198 }
199
200 return false;
201 }
202
203#if defined( _WIN32 )
204 // Cloud-sync mounts (OneDrive/Drive/Dropbox) can reject MoveFileEx when the
205 // target has FILE_ATTRIBUTE_HIDDEN. Clear blocking bits here; the snapshot
206 // restores them below. POSIX rename() requires write on the containing
207 // directory only, not on the target file, so MakeWriteable is unnecessary.
208 MakeWriteable( aTargetPath );
209#endif
210 }
211
212 wxString renameError;
213 const bool renamed = AtomicRename( aTempPath, aTargetPath, &renameError );
214
215 if( targetExists )
216 {
217 // Unconditional re-apply. On rename failure this rolls back the MakeWriteable
218 // mutation on the original target. On rename success it restores HIDDEN/
219 // READONLY bits to the new file, since SetFileSecurity (used by
220 // DuplicatePermissions) copies ACLs but not those attribute bits. A failure
221 // here is logged but not fatal: the rename has already committed (or was
222 // going to be reported as failed below), so the user's data is consistent;
223 // only the attribute bits are off.
224 if( !ApplyTargetAttributes( aTargetPath, snapshot ) )
225 {
226 wxLogWarning( wxT( "Could not restore file attributes on '%s' after save" ),
227 aTargetPath );
228 }
229 }
230
231 if( !renamed )
232 {
233 if( aError )
234 {
235 *aError = wxString::Format( wxT( "Cannot rename temp file over '%s': %s" ),
236 aTargetPath, renameError );
237 }
238
239 return false;
240 }
241
242 wxFileName dst( aTargetPath );
243 wxString dirPath = dst.GetPath();
244
245 // A bare filename has no directory component; fall back to CWD so the dir fsync
246 // lands on the filesystem that actually holds the file.
247 if( dirPath.IsEmpty() )
248 dirPath = wxT( "." );
249
250 if( !FlushDirectory( dirPath ) )
251 {
252 // The rename has already committed, but without a dir fsync it may not survive
253 // power loss. Report so callers can warn the user; the file itself is present.
254 if( aError )
255 *aError = wxString::Format( wxT( "Cannot flush directory '%s' to disk" ), dirPath );
256
257 return false;
258 }
259
260 return true;
261}
262
263
264bool KIPLATFORM::IO::AtomicWriteFile( const wxString& aTargetPath, const void* aData, size_t aSize,
265 wxString* aError )
266{
267 wxString target = ResolveSymlinkTarget( aTargetPath );
268 wxString tempPath;
269 FILE* fp = OpenUniqueSiblingTempFile( target, wxT( "wb" ), &tempPath, aError );
270
271 if( !fp )
272 return false;
273
274 if( aSize > 0 && std::fwrite( aData, 1, aSize, fp ) != aSize )
275 {
276 if( aError )
277 *aError = wxString::Format( wxT( "Write failed to '%s'" ), tempPath );
278
279 std::fclose( fp );
280 wxRemoveFile( tempPath );
281 return false;
282 }
283
284 if( !FlushToDisk( fp ) )
285 {
286 if( aError )
287 *aError = wxString::Format( wxT( "fsync failed on '%s'" ), tempPath );
288
289 std::fclose( fp );
290 wxRemoveFile( tempPath );
291 return false;
292 }
293
294 std::fclose( fp );
295
296 if( !CommitTempFile( tempPath, target, aError ) )
297 {
298 // CommitTempFile can fail after a successful rename (e.g. dir fsync error),
299 // in which case tempPath no longer exists. Suppress the expected log noise.
300 wxLogNull logNoise;
301 wxRemoveFile( tempPath );
302 return false;
303 }
304
305 return true;
306}
307
308
309
310
311
312void KIPLATFORM::IO::MAPPED_FILE::readIntoBuffer( const wxString& aFileName )
313{
314 FILE* fp = wxFopen( aFileName, wxS( "rb" ) );
315
316 if( !fp )
317 throw std::runtime_error( std::string( "Cannot open file: " ) + aFileName.ToStdString() );
318
319 fseek( fp, 0, SEEK_END );
320 long len = ftell( fp );
321
322 if( len < 0 )
323 {
324 fclose( fp );
325 throw std::runtime_error( std::string( "Cannot determine file size: " )
326 + aFileName.ToStdString() );
327 }
328
329 m_fallbackBuffer.resize( static_cast<size_t>( len ) );
330 fseek( fp, 0, SEEK_SET );
331
332 size_t bytesRead = fread( m_fallbackBuffer.data(), 1, static_cast<size_t>( len ), fp );
333 fclose( fp );
334
335 if( bytesRead != static_cast<size_t>( len ) )
336 {
337 throw std::runtime_error( std::string( "Failed to read file: " )
338 + aFileName.ToStdString() );
339 }
340
341 m_data = m_fallbackBuffer.data();
342 m_size = m_fallbackBuffer.size();
343}
void readIntoBuffer(const wxString &aFileName)
const uint8_t * m_data
Definition io.h:56
std::vector< uint8_t > m_fallbackBuffer
Definition io.h:66
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.
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 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 CommitTempFile(const wxString &aTempPath, const wxString &aTargetPath, wxString *aError=nullptr)
Completes an atomic save.
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
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
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