E-MailRelay
gfile_unix.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gfile_unix.cpp
19///
20
21#include "gdef.h"
22#include "gfile.h"
23#include "gstr.h"
24#include "gprocess.h"
25#include "glog.h"
26#include "gassert.h"
27#include <vector>
28#include <sstream>
29#include <cerrno> // ENOENT etc
30#include <sys/types.h>
31#include <sys/stat.h>
32#include <unistd.h>
33#include <fcntl.h>
34
35namespace G
36{
37 namespace FileImp
38 {
39 bool removeImp( const char * path , int * e ) noexcept ;
40 std::pair<bool,mode_t> newmode( mode_t , const std::string & ) ;
41 std::pair<std::time_t,unsigned int> mtime( struct stat & statbuf ) noexcept
42 {
43 #if GCONFIG_HAVE_STATBUF_TIMESPEC
44 return { statbuf.st_mtimespec.tv_sec , statbuf.st_mtimespec.tv_nsec/1000U } ;
45 #else
46 #if GCONFIG_HAVE_STATBUF_NSEC
47 return { statbuf.st_mtime , statbuf.st_mtim.tv_nsec/1000U } ;
48 #else
49 return { statbuf.st_mtime , 0U } ;
50 #endif
51 #endif
52 }
53 }
54}
55
56void G::File::open( std::ofstream & ofstream , const Path & path )
57{
58 ofstream.open( path.cstr() , std::ios_base::out | std::ios_base::binary ) ;
59}
60
61void G::File::open( std::ofstream & ofstream , const Path & path , Text )
62{
63 ofstream.open( path.cstr() , std::ios_base::out ) ;
64}
65
66void G::File::open( std::ofstream & ofstream , const Path & path , Append )
67{
68 ofstream.open( path.cstr() , std::ios_base::app | std::ios_base::binary ) ;
69}
70
71void G::File::open( std::ifstream & ifstream , const Path & path )
72{
73 ifstream.open( path.cstr() , std::ios_base::in | std::ios_base::binary ) ;
74}
75
76#ifndef G_LIB_SMALL
77void G::File::open( std::ifstream & ifstream , const Path & path , Text )
78{
79 ifstream.open( path.cstr() , std::ios_base::in ) ;
80}
81#endif
82
83std::filebuf * G::File::open( std::filebuf & fb , const Path & path , InOut inout )
84{
85 return
86 inout == InOut::In ?
87 fb.open( path.cstr() , std::ios_base::in | std::ios_base::binary ) :
88 fb.open( path.cstr() , std::ios_base::out | std::ios_base::binary ) ;
89}
90
91int G::File::open( const Path & path , InOutAppend mode , bool ) noexcept
92{
93 static_assert( noexcept(path.cstr()) , "" ) ;
94 const char * path_cstr = path.cstr() ;
95 if( mode == InOutAppend::In )
96 return ::open( path_cstr , O_RDONLY ) ; // NOLINT
97 else if( mode == InOutAppend::Out )
98 return ::open( path_cstr , O_WRONLY|O_CREAT|O_TRUNC , 0666 ) ; // NOLINT
99 else if( mode == InOutAppend::OutNoCreate )
100 return ::open( path_cstr , O_WRONLY , 0666 ) ; // NOLINT
101 else
102 return ::open( path_cstr , O_WRONLY|O_CREAT|O_APPEND , 0666 ) ; // NOLINT
103}
104
105#ifndef G_LIB_SMALL
106int G::File::open( const Path & path , CreateExclusive ) noexcept
107{
108 static_assert( noexcept(path.cstr()) , "" ) ;
109 return ::open( path.cstr() , O_WRONLY|O_CREAT|O_EXCL , 0666 ) ; // NOLINT
110}
111#endif
112
113#ifndef G_LIB_SMALL
114std::FILE * G::File::fopen( const Path & path , const char * mode ) noexcept
115{
116 return std::fopen( path.cstr() , mode ) ;
117}
118#endif
119
120bool G::File::probe( const Path & path ) noexcept
121{
122 static_assert( noexcept(path.cstr()) , "" ) ;
123 static_assert( noexcept(File::remove(path,std::nothrow)) , "" ) ;
124
125 int fd = ::open( path.cstr() , O_WRONLY|O_CREAT|O_EXCL , 0666 ) ; // NOLINT
126 if( fd < 0 )
127 return false ;
128
129 File::remove( path , std::nothrow ) ;
130 ::close( fd ) ;
131 return true ;
132}
133
134#ifndef G_LIB_SMALL
135void G::File::create( const Path & path )
136{
137 int fd = ::open( path.cstr() , O_RDONLY|O_CREAT , 0666 ) ; // NOLINT
138 if( fd < 0 )
139 throw CannotCreate( path.str() ) ;
140 ::close( fd ) ;
141}
142#endif
143
144bool G::File::renameOnto( const Path & from , const Path & to , std::nothrow_t ) noexcept
145{
146 return 0 == std::rename( from.cstr() , to.cstr() ) ; // overwrites 'to'
147}
148
149ssize_t G::File::read( int fd , char * p , std::size_t n ) noexcept
150{
151 return ::read( fd , p , n ) ;
152}
153
154ssize_t G::File::write( int fd , const char * p , std::size_t n ) noexcept
155{
156 return ::write( fd , p , n ) ;
157}
158
159void G::File::close( int fd ) noexcept
160{
161 ::close( fd ) ;
162}
163
164bool G::FileImp::removeImp( const char * path , int * e ) noexcept
165{
166 bool ok = path && 0 == std::remove( path ) ;
167 if( e )
168 *e = ok ? 0 : ( path ? Process::errno_() : EINVAL ) ;
169 return ok ;
170}
171
172bool G::File::cleanup( const Cleanup::Arg & arg ) noexcept
173{
174 return FileImp::removeImp( arg.str() , nullptr ) ;
175}
176
177bool G::File::remove( const Path & path , std::nothrow_t ) noexcept
178{
179 static_assert( noexcept(path.cstr()) , "" ) ;
180 return FileImp::removeImp( path.cstr() , nullptr ) ;
181}
182
183void G::File::remove( const Path & path )
184{
185 int e = 0 ;
186 bool ok = FileImp::removeImp( path.cstr() , &e ) ;
187 if( !ok )
188 {
189 G_WARNING( "G::File::remove: cannot delete file [" << path << "]: " << Process::strerror(e) ) ;
190 throw CannotRemove( path.str() , Process::strerror(e) ) ;
191 }
192}
193
194int G::File::mkdirImp( const Path & dir ) noexcept
195{
196 int rc = ::mkdir( dir.cstr() , 0777 ) ; // open permissions, but limited by umask
197 if( rc == 0 )
198 {
199 return 0 ;
200 }
201 else
202 {
203 int e = G::Process::errno_() ;
204 if( e == 0 ) e = EINVAL ;
205 return e ;
206 }
207}
208
209G::File::Stat G::File::statImp( const char * path , bool symlink_nofollow ) noexcept
210{
211 Stat s ;
212 struct stat statbuf {} ;
213 if( 0 == ( symlink_nofollow ? (::lstat(path,&statbuf)) : (::stat(path,&statbuf)) ) )
214 {
215 s.error = 0 ;
216 s.enoent = false ;
217 s.eaccess = false ;
218 s.is_link = (statbuf.st_mode & S_IFMT) == S_IFLNK ; // NOLINT
219 s.is_dir = (statbuf.st_mode & S_IFMT) == S_IFDIR ; // NOLINT
220 s.is_executable = !!(statbuf.st_mode & S_IXUSR) && !!(statbuf.st_mode & S_IRUSR) ; // indicitive // NOLINT
221 s.is_empty = statbuf.st_size == 0 ;
222 s.mtime_s = FileImp::mtime(statbuf).first ;
223 s.mtime_us = FileImp::mtime(statbuf).second ;
224 s.mode = static_cast<unsigned long>( statbuf.st_mode & mode_t(07777) ) ; // NOLINT
225 s.size = static_cast<unsigned long long>( statbuf.st_size ) ;
226 s.blocks = static_cast<unsigned long long>(statbuf.st_size) >> 24U ;
227 s.uid = statbuf.st_uid ;
228 s.gid = statbuf.st_gid ;
229 s.inherit = s.is_dir && ( G::is_bsd() || ( statbuf.st_mode & S_ISGID ) ) ;
230 }
231 else
232 {
233 int error = Process::errno_() ;
234 s.error = error ? error : EINVAL ;
235 s.enoent = error == ENOENT || error == ENOTDIR ;
236 s.eaccess = error == EACCES ;
237 }
238 return s ;
239}
240
241bool G::File::existsImp( const char * path , bool & enoent , bool & eaccess ) noexcept
242{
243 Stat s = statImp( path ) ;
244 if( s.error )
245 {
246 enoent = s.enoent ;
247 eaccess = s.eaccess ;
248 }
249 return s.error == 0 ;
250}
251
252bool G::File::chmodx( const Path & path , bool do_throw )
253{
254 Stat s = statImp( path.cstr() ) ;
255 mode_t mode = s.error ? mode_t(0777) : mode_t(s.mode) ;
256
257 mode |= ( S_IRUSR | S_IXUSR ) ; // add user-read and user-executable // NOLINT
258 if( mode & S_IRGRP ) mode |= S_IXGRP ; // add group-executable iff group-read // NOLINT
259 if( mode & S_IROTH ) mode |= S_IXOTH ; // add world-executable iff world-read // NOLINT
260
261 // apply the current umask
262 mode_t mask = ::umask( 0 ) ; ::umask( mask ) ;
263 mode &= ~mask ;
264
265 bool ok = 0 == ::chmod( path.cstr() , mode ) ;
266 if( !ok && do_throw )
267 throw CannotChmod( path.str() ) ;
268 return ok ;
269}
270
271#ifndef G_LIB_SMALL
272void G::File::chmod( const Path & path , const std::string & spec )
273{
274 if( !chmod( path , spec , std::nothrow ) )
275 throw CannotChmod( path.str() ) ;
276}
277#endif
278
279bool G::File::chmod( const Path & path , const std::string & spec , std::nothrow_t )
280{
281 if( spec.empty() )
282 {
283 return false ;
284 }
285 else if( spec.find_first_not_of("01234567") == std::string::npos )
286 {
287 mode_t mode = static_cast<mode_t>( strtoul( spec.c_str() , nullptr , 8 ) ) ;
288 return mode <= 07777 && 0 == ::chmod( path.cstr() , mode ) ;
289 }
290 else
291 {
292 Stat s = statImp( path.cstr() ) ;
293 if( s.error )
294 return false ;
295 std::pair<bool,mode_t> pair = FileImp::newmode( s.mode , spec ) ;
296 return pair.first && 0 == ::chmod( path.cstr() , pair.second ) ;
297 }
298}
299
300std::pair<bool,mode_t> G::FileImp::newmode( mode_t mode , const std::string & spec_in )
301{
302 mode &= mode_t(07777) ;
303 G::StringArray spec_list = G::Str::splitIntoFields( spec_in , ',' ) ;
304 bool ok = !spec_list.empty() ;
305 for( auto spec : spec_list )
306 {
307 if( spec.size() >= 2U &&
308 ( spec.at(0U) == '+' || spec.at(0U) == '-' || spec.at(0U) == '=' ) )
309 {
310 spec.insert( 0U , "a" ) ;
311 }
312 if( spec.size() >= 3U &&
313 ( spec.at(0U) == 'u' || spec.at(0U) == 'g' || spec.at(0U) == 'o' || spec.at(0U) == 'a' ) &&
314 ( spec.at(1U) == '+' || spec.at(1U) == '-' || spec.at(1U) == '=' ) )
315 {
316 mode_t part = 0 ;
317 mode_t special = 0 ;
318 for( const char * p = spec.c_str()+2 ; *p ; p++ )
319 {
320 if( *p == 'r' )
321 part |= mode_t(4) ;
322 else if( *p == 'w' )
323 part |= mode_t(2) ;
324 else if( *p == 'x' )
325 part |= mode_t(1) ;
326 else if( *p == 's' && spec[0] == 'u' )
327 special |= S_ISUID ; // NOLINT
328 else if( *p == 's' && spec[0] == 'g' )
329 special |= S_ISGID ; // NOLINT
330 else if( *p == 't' && spec[0] == 'o' )
331 special |= S_ISVTX ; // NOLINT
332 else
333 ok = false ;
334 }
335 unsigned int shift = spec[0]=='u' ? 6U : (spec[0]=='g'?3U:0U) ;
336 if( spec[0] == 'a' )
337 {
338 mode_t mask = umask(0) ; umask( mask ) ;
339 part = ( ((part<<6U)|(part<<3U)|part) & ~mask ) ;
340 }
341 if( spec[1] == '=' && spec[0] == 'a' )
342 {
343 mode = part ;
344 }
345 else if( spec[1] == '=' )
346 {
347 mode_t clearbits = (mode_t(7)<<shift) | (spec[0]=='u'?mode_t(S_ISUID):(spec[0]=='g'?mode_t(S_ISGID):mode_t(S_ISVTX))) ;
348 mode &= ~clearbits ;
349 mode |= (part<<shift) ;
350 mode |= special ;
351 }
352 else if( spec[1] == '+' )
353 {
354 mode |= ( (part<<shift) | special ) ;
355 }
356 else
357 {
358 mode &= ~( (part<<shift) | special ) ;
359 }
360 }
361 else
362 {
363 ok = false ;
364 }
365 }
366 return { ok , mode } ;
367}
368
369#ifndef G_LIB_SMALL
370void G::File::chgrp( const Path & path , const std::string & group )
371{
372 bool ok = 0 == ::chown( path.cstr() , -1 , Identity::lookupGroup(group) ) ;
373 if( !ok )
374 throw CannotChgrp( path.str() ) ;
375}
376#endif
377
378#ifndef G_LIB_SMALL
379bool G::File::chgrp( const Path & path , const std::string & group , std::nothrow_t )
380{
381 return 0 == ::chown( path.cstr() , -1 , Identity::lookupGroup(group) ) ;
382}
383#endif
384
385bool G::File::chgrp( const Path & path , gid_t group_id , std::nothrow_t )
386{
387 return 0 == ::chown( path.cstr() , -1 , group_id ) ;
388}
389
390bool G::File::hardlink( const Path & src , const Path & dst , std::nothrow_t )
391{
392 return 0 == ::link( src.cstr() , dst.cstr() ) ;
393}
394
395#ifndef G_LIB_SMALL
396void G::File::link( const Path & target , const Path & new_link )
397{
398 if( linked(target,new_link) ) // optimisation
399 return ;
400
401 if( exists(new_link) )
402 File::remove( new_link , std::nothrow ) ;
403
404 int error = linkImp( target.cstr() , new_link.cstr() ) ;
405
406 if( error != 0 )
407 {
408 std::ostringstream ss ;
409 ss << "[" << new_link << "] -> [" << target << "] " "(" << error << ")" ;
410 throw CannotLink( ss.str() ) ;
411 }
412}
413#endif
414
415#ifndef G_LIB_SMALL
416bool G::File::link( const Path & target , const Path & new_link , std::nothrow_t )
417{
418 if( linked(target,new_link) ) // optimisation
419 return true ;
420
421 if( exists(new_link) )
422 File::remove( new_link , std::nothrow ) ;
423
424 return 0 == linkImp( target.cstr() , new_link.cstr() ) ;
425}
426#endif
427
428int G::File::linkImp( const char * target , const char * new_link )
429{
430 int rc = ::symlink( target , new_link ) ;
431 int error = Process::errno_() ;
432 return rc == 0 ? 0 : (error?error:EINVAL) ;
433}
434
435#ifndef G_LIB_SMALL
437{
438 Path result = readlink( link , std::nothrow ) ;
439 if( result.empty() )
440 throw CannotReadLink( link.str() ) ;
441 return result ;
442}
443#endif
444
445G::Path G::File::readlink( const Path & link , std::nothrow_t )
446{
447 Path result ;
448 struct stat statbuf {} ;
449 int rc = ::lstat( link.cstr() , &statbuf ) ;
450 if( rc == 0 )
451 {
452 std::size_t buffer_size = statbuf.st_size ? (statbuf.st_size+1U) : 1024U ;
453 std::vector<char> buffer( buffer_size , '\0' ) ;
454 ssize_t nread = ::readlink( link.cstr() , buffer.data() , buffer.size() ) ;
455
456 // (filesystem race can cause truncation -- treat as an error)
457 if( nread > 0 && static_cast<std::size_t>(nread) < buffer.size() )
458 {
459 G_ASSERT( buffer.at(static_cast<std::size_t>(nread-1)) != '\0' ) ; // readlink does not null-terminate
460 result = Path( std::string( buffer.data() , static_cast<std::size_t>(nread) ) ) ;
461 }
462 }
463 return result ;
464}
465
466bool G::File::linked( const Path & target , const Path & new_link )
467{
468 // see if already linked correctly - errors and overflows are not fatal
469 return readlink(new_link,std::nothrow) == target ;
470}
471
472std::streamoff G::File::seek( int fd , std::streamoff offset , Seek origin ) noexcept
473{
474 off_t rc = ::lseek( fd , static_cast<off_t>(offset) ,
475 origin == Seek::Start ? SEEK_SET : ( origin == Seek::End ? SEEK_END : SEEK_CUR ) ) ;
476 return static_cast<std::streamoff>(rc) ;
477}
478
479#ifndef G_LIB_SMALL
480void G::File::setNonBlocking( int fd ) noexcept
481{
482 int flags = ::fcntl( fd , F_GETFL ) ; // NOLINT
483 if( flags != -1 )
484 {
485 flags |= O_NONBLOCK ; // NOLINT
486 GDEF_IGNORE_RETURN ::fcntl( fd , F_SETFL , flags ) ; // NOLINT
487 }
488}
489#endif
490
An overload discriminator for G::File::open().
Definition: gfile.h:62
An overload discriminator for G::File::open().
Definition: gfile.h:64
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
static void close(int fd) noexcept
Calls ::close() or equivalent.
Definition: gfile_unix.cpp:159
static void link(const Path &target, const Path &new_link)
Creates a symlink.
Definition: gfile_unix.cpp:396
static std::streamoff seek(int fd, std::streamoff offset, Seek) noexcept
Does ::lseek() or equivalent.
Definition: gfile_unix.cpp:472
static void setNonBlocking(int fd) noexcept
Sets the file descriptor to non-blocking mode.
Definition: gfile_unix.cpp:480
static ssize_t write(int fd, const char *, std::size_t) noexcept
Calls ::write() or equivalent.
Definition: gfile_unix.cpp:154
static void chmod(const Path &file, const std::string &spec)
Sets the file permissions.
Definition: gfile_unix.cpp:272
static bool remove(const Path &path, std::nothrow_t) noexcept
Deletes the file or directory. Returns false on error.
Definition: gfile_unix.cpp:177
static void chmodx(const Path &file)
Makes the file executable. Throws on error.
Definition: gfile.cpp:229
static ssize_t read(int fd, char *, std::size_t) noexcept
Calls read() or equivalent.
Definition: gfile_unix.cpp:149
static bool renameOnto(const Path &from, const Path &to, std::nothrow_t) noexcept
Renames the file, deleting 'to' first if necessary.
Definition: gfile_unix.cpp:144
static void create(const Path &)
Creates the file if it does not exist.
Definition: gfile_unix.cpp:135
static G::Path readlink(const Path &link)
Reads a symlink. Throws on error.
Definition: gfile_unix.cpp:436
static std::FILE * fopen(const Path &, const char *mode) noexcept
Calls std::fopen().
Definition: gfile_unix.cpp:114
static bool hardlink(const Path &src, const Path &dst, std::nothrow_t)
Creates a hard link.
Definition: gfile_unix.cpp:390
static bool probe(const Path &) noexcept
Creates and deletes a temporary probe file.
Definition: gfile_unix.cpp:120
static bool cleanup(const Cleanup::Arg &path_arg) noexcept
Deletes the file.
Definition: gfile_unix.cpp:172
static void chgrp(const Path &file, const std::string &group)
Sets the file group ownership. Throws on error.
Definition: gfile_unix.cpp:370
static gid_t lookupGroup(const std::string &group)
Does a groupname lookup.
A Path object represents a file system path.
Definition: gpath.h:82
const value_type * cstr() const noexcept
Returns the path's c-string.
Definition: gpath.h:249
std::string str() const
Returns the path string.
Definition: gpath.h:243
bool empty() const noexcept
Returns true if the path is empty.
Definition: gpath.h:237
static std::string strerror(int errno_)
Translates an 'errno' value into a meaningful diagnostic string.
static int errno_(const SignalSafe &=G::SignalSafe()) noexcept
Returns the process's current 'errno' value.
static void splitIntoFields(std::string_view in, StringArray &out, char sep, char escape='\0', bool remove_escapes=true)
Splits the string into fields.
Definition: gstr.cpp:1191
Low-level classes.
Definition: garg.h:36
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
Opaque leaky string pointer wrapper created by G::Cleanup::arg().
Definition: gcleanup.h:55
An overload discriminator for G::File::open().
Definition: gfile.h:66
A portable 'struct stat'.
Definition: gfile.h:68