diff options
author | Tony Cook <tony@develop-help.com> | 2020-10-14 13:27:50 +1100 |
---|---|---|
committer | Tony Cook <tony@develop-help.com> | 2020-12-01 15:29:33 +1100 |
commit | e935ef333b3eab54a766de93fad1369f76ddea49 (patch) | |
tree | ea085a4dd5daf2e5f236818ba2a9ca8309eabb60 /win32 | |
parent | 680b2c5ee3b53c627074192b3cf14416a24da6ea (diff) | |
download | perl-e935ef333b3eab54a766de93fad1369f76ddea49.tar.gz |
Win32: implement our own stat(), and hence our own utime
This fixes at least two problems:
- unlike UCRT, the MSVCRT used for gcc builds has a bug converting
a FILETIME in an unlike current DST state, returning a time
offset by an hour. Fixes GH #6080
- the MSVCRT apparently uses FindFirstFile() to fetch file
information, but this doesn't follow symlinks(), so stat()
ends up returning information about the symlink(), not the
underlying file. This isn't an issue with the UCRT which
opens the file as this implementation does.
Currently this code calculates the time_t for st_*time, and the
other way for utime() using a simple multiplication and offset
between time_t and FILETIME values, but this may be incorrect
if leap seconds are enabled.
This code also requires Vista or later.
Some of this is based on code by Tomasz Konojacki (xenu).
Diffstat (limited to 'win32')
-rw-r--r-- | win32/win32.c | 397 |
1 files changed, 188 insertions, 209 deletions
diff --git a/win32/win32.c b/win32/win32.c index 162ef62de0..829bdfbc60 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -10,16 +10,14 @@ #define PERLIO_NOT_STDIO 0 #define WIN32_LEAN_AND_MEAN #define WIN32IO_IS_STDIO +/* for CreateSymbolicLinkA() etc */ +#define _WIN32_WINNT 0x0601 #include <tchar.h> #ifdef __GNUC__ # define Win32_Winsock #endif -#ifndef _WIN32_WINNT -# define _WIN32_WINNT 0x0500 /* needed for CreateHardlink() etc. */ -#endif - #include <windows.h> #ifndef HWND_MESSAGE @@ -164,6 +162,8 @@ static HWND get_hwnd_delay(pTHX, long child, DWORD tries); static void win32_csighandler(int sig); #endif +static void translate_to_errno(void); + START_EXTERN_C HANDLE w32_perldll_handle = INVALID_HANDLE_VALUE; char w32_module_name[MAX_PATH+1]; @@ -180,6 +180,22 @@ static HKEY HKCU_Perl_hnd; static HKEY HKLM_Perl_hnd; #endif +/* the time_t epoch start time as a filetime expressed as a large integer */ +static ULARGE_INTEGER time_t_epoch_base_filetime; + +static const SYSTEMTIME time_t_epoch_base_systemtime = { + 1970, /* wYear */ + 1, /* wMonth */ + 0, /* wDayOfWeek */ + 1, /* wDay */ + 0, /* wHour */ + 0, /* wMinute */ + 0, /* wSecond */ + 0 /* wMilliseconds */ +}; + +#define FILETIME_CHUNKS_PER_SECOND (10000000UL) + #ifdef SET_INVALID_PARAMETER_HANDLER static BOOL silent_invalid_parameter_handler = FALSE; @@ -1455,143 +1471,136 @@ win32_kill(int pid, int sig) return -1; } +PERL_STATIC_INLINE +time_t +translate_ft_to_time_t(FILETIME ft) { + /* Based on Win32::UTCTime. + Older CRTs (including MSVCRT used for gcc builds) product + strange behaviour when the specified time and the current time + differ on whether DST was in effect, this code doesnt have that + problem. + */ + ULARGE_INTEGER u; + u.LowPart = ft.dwLowDateTime; + u.HighPart = ft.dwHighDateTime; + return (u.QuadPart - time_t_epoch_base_filetime.QuadPart) / FILETIME_CHUNKS_PER_SECOND; +} + +static int +win32_stat_low(HANDLE handle, const char *path, STRLEN len, Stat_t *sbuf) { + DWORD type = GetFileType(handle); + BY_HANDLE_FILE_INFORMATION bhi; + + Zero(sbuf, 1, Stat_t); + + type &= ~FILE_TYPE_REMOTE; + + switch (type) { + case FILE_TYPE_DISK: + if (GetFileInformationByHandle(handle, &bhi)) { + sbuf->st_dev = bhi.dwVolumeSerialNumber; + sbuf->st_ino = bhi.nFileIndexHigh; + sbuf->st_ino <<= 32; + sbuf->st_ino |= bhi.nFileIndexLow; + sbuf->st_nlink = bhi.nNumberOfLinks; + sbuf->st_uid = 0; + sbuf->st_gid = 0; + /* ucrt sets this to the drive letter for + stat(), lets not reproduce that mistake */ + sbuf->st_rdev = 0; + sbuf->st_size = bhi.nFileSizeHigh; + sbuf->st_size <<= 32; + sbuf->st_size |= bhi.nFileSizeLow; + + sbuf->st_atime = translate_ft_to_time_t(bhi.ftLastAccessTime); + sbuf->st_mtime = translate_ft_to_time_t(bhi.ftLastWriteTime); + sbuf->st_ctime = translate_ft_to_time_t(bhi.ftCreationTime); + + if (bhi.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + sbuf->st_mode = _S_IFDIR | _S_IREAD | _S_IEXEC; + /* duplicate the logic from the end of the old win32_stat() */ + if (!(bhi.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) { + sbuf->st_mode |= S_IWRITE; + } + } + else { + char path_buf[MAX_PATH+1]; + sbuf->st_mode = _S_IFREG; + + if (!path) { + len = GetFinalPathNameByHandleA(handle, path_buf, sizeof(path_buf), 0); + /* < to ensure there's space for the \0 */ + if (len && len < sizeof(path_buf)) { + path = path_buf; + } + } + + if (path && len > 4 && + (_stricmp(path + len - 4, ".exe") == 0 || + _stricmp(path + len - 4, ".bat") == 0 || + _stricmp(path + len - 4, ".cmd") == 0 || + _stricmp(path + len - 4, ".com") == 0)) { + sbuf->st_mode |= _S_IEXEC; + } + if (!(bhi.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) { + sbuf->st_mode |= _S_IWRITE; + } + sbuf->st_mode |= _S_IREAD; + } + } + else { + translate_to_errno(); + return -1; + } + break; + + case FILE_TYPE_CHAR: + case FILE_TYPE_PIPE: + sbuf->st_mode = (type == FILE_TYPE_CHAR) ? _S_IFCHR : _S_IFIFO; + if (handle == GetStdHandle(STD_INPUT_HANDLE) || + handle == GetStdHandle(STD_OUTPUT_HANDLE) || + handle == GetStdHandle(STD_ERROR_HANDLE)) { + sbuf->st_mode |= _S_IWRITE | _S_IREAD; + } + break; + + default: + return -1; + } + + /* owner == user == group */ + sbuf->st_mode |= (sbuf->st_mode & 0700) >> 3; + sbuf->st_mode |= (sbuf->st_mode & 0700) >> 6; + + return 0; +} + DllExport int win32_stat(const char *path, Stat_t *sbuf) { - char buffer[MAX_PATH+1]; - int l = strlen(path); + size_t l = strlen(path); dTHX; - int res; - int nlink = 1; - unsigned __int64 ino = 0; - DWORD vol = 0; BOOL expect_dir = FALSE; - struct _stati64 st; - - if (l > 1) { - switch(path[l - 1]) { - /* FindFirstFile() and stat() are buggy with a trailing - * slashes, except for the root directory of a drive */ - case '\\': - case '/': - if (l > sizeof(buffer)) { - errno = ENAMETOOLONG; - return -1; - } - --l; - strncpy(buffer, path, l); - /* remove additional trailing slashes */ - while (l > 1 && (buffer[l-1] == '/' || buffer[l-1] == '\\')) - --l; - /* add back slash if we otherwise end up with just a drive letter */ - if (l == 2 && isALPHA(buffer[0]) && buffer[1] == ':') - buffer[l++] = '\\'; - buffer[l] = '\0'; - path = buffer; - expect_dir = TRUE; - break; - - /* FindFirstFile() is buggy with "x:", so add a dot :-( */ - case ':': - if (l == 2 && isALPHA(path[0])) { - buffer[0] = path[0]; - buffer[1] = ':'; - buffer[2] = '.'; - buffer[3] = '\0'; - l = 3; - path = buffer; - } - break; - } - } + int result; + HANDLE handle; path = PerlDir_mapA(path); l = strlen(path); - if (!w32_sloppystat) { - /* We must open & close the file once; otherwise file attribute changes */ - /* might not yet have propagated to "other" hard links of the same file. */ - /* This also gives us an opportunity to determine the number of links. */ - HANDLE handle = CreateFileA(path, 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (handle != INVALID_HANDLE_VALUE) { - BY_HANDLE_FILE_INFORMATION bhi; - if (GetFileInformationByHandle(handle, &bhi)) { - nlink = bhi.nNumberOfLinks; - ino = bhi.nFileIndexHigh; - ino <<= 32; - ino |= bhi.nFileIndexLow; - vol = bhi.dwVolumeSerialNumber; - } - CloseHandle(handle); - } - else { - DWORD err = GetLastError(); - /* very common case, skip CRT stat and its also failing syscalls */ - if(err == ERROR_FILE_NOT_FOUND) { - errno = ENOENT; - return -1; - } - } - } - - /* path will be mapped correctly above */ - res = _stati64(path, sbuf); - sbuf->st_dev = vol; - sbuf->st_ino = ino; - sbuf->st_mode = st.st_mode; - sbuf->st_nlink = nlink; - sbuf->st_uid = st.st_uid; - sbuf->st_gid = st.st_gid; - sbuf->st_rdev = st.st_rdev; - sbuf->st_size = st.st_size; - sbuf->st_atime = st.st_atime; - sbuf->st_mtime = st.st_mtime; - sbuf->st_ctime = st.st_ctime; - - if (res < 0) { - /* CRT is buggy on sharenames, so make sure it really isn't. - * XXX using GetFileAttributesEx() will enable us to set - * sbuf->st_*time (but note that's not available on the - * Windows of 1995) */ - DWORD r = GetFileAttributesA(path); - if (r != 0xffffffff && (r & FILE_ATTRIBUTE_DIRECTORY)) { - /* sbuf may still contain old garbage since stat() failed */ - Zero(sbuf, 1, Stat_t); - sbuf->st_mode = S_IFDIR | S_IREAD; - errno = 0; - if (!(r & FILE_ATTRIBUTE_READONLY)) - sbuf->st_mode |= S_IWRITE | S_IEXEC; - return 0; - } + handle = + CreateFileA(path, FILE_READ_ATTRIBUTES, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (handle != INVALID_HANDLE_VALUE) { + result = win32_stat_low(handle, path, l, sbuf); + CloseHandle(handle); } else { - if (l == 3 && isALPHA(path[0]) && path[1] == ':' - && (path[2] == '\\' || path[2] == '/')) - { - /* The drive can be inaccessible, some _stat()s are buggy */ - if (!GetVolumeInformationA(path,NULL,0,NULL,NULL,NULL,NULL,0)) { - errno = ENOENT; - return -1; - } - } - if (expect_dir && !S_ISDIR(sbuf->st_mode)) { - errno = ENOTDIR; - return -1; - } - if (S_ISDIR(sbuf->st_mode)) { - /* Ensure the "write" bit is switched off in the mode for - * directories with the read-only attribute set. Some compilers - * switch it on for directories, which is technically correct - * (directories are indeed always writable unless denied by DACLs), - * but we want stat() and -w to reflect the state of the read-only - * attribute for symmetry with chmod(). */ - DWORD r = GetFileAttributesA(path); - if (r != 0xffffffff && (r & FILE_ATTRIBUTE_READONLY)) { - sbuf->st_mode &= ~S_IWRITE; - } - } + translate_to_errno(); + result = -1; } - return res; + + return result; } static void @@ -1600,9 +1609,6 @@ translate_to_errno(void) /* This isn't perfect, eg. Win32 returns ERROR_ACCESS_DENIED for both permissions errors and if the source is a directory, while POSIX wants EACCES and EPERM respectively. - - Determined by experimentation on Windows 7 x64 SP1, since MS - don't document what error codes are returned. */ switch (GetLastError()) { case ERROR_BAD_NET_NAME: @@ -1618,9 +1624,11 @@ translate_to_errno(void) errno = EEXIST; break; case ERROR_ACCESS_DENIED: - case ERROR_PRIVILEGE_NOT_HELD: errno = EACCES; break; + case ERROR_PRIVILEGE_NOT_HELD: + errno = EPERM; + break; case ERROR_NOT_SAME_DEVICE: errno = EXDEV; break; @@ -1776,7 +1784,6 @@ DllExport int win32_lstat(const char *path, Stat_t *sbuf) { HANDLE f; - int fd; int result; DWORD attr = GetFileAttributes(path); /* doesn't follow symlinks */ @@ -1801,12 +1808,13 @@ win32_lstat(const char *path, Stat_t *sbuf) return win32_stat(path, sbuf); } - fd = win32_open_osfhandle((intptr_t)f, 0); - result = win32_fstat(fd, sbuf); + result = win32_stat_low(f, NULL, 0, sbuf); + CloseHandle(f); + if (result != -1){ sbuf->st_mode = (sbuf->st_mode & ~_S_IFMT) | _S_IFLNK; } - close(fd); + return result; } @@ -2162,27 +2170,17 @@ win32_times(struct tms *timebuf) return process_time_so_far; } -/* fix utime() so it works on directories in NT */ static BOOL filetime_from_time(PFILETIME pFileTime, time_t Time) { - struct tm *pTM = localtime(&Time); - SYSTEMTIME SystemTime; - FILETIME LocalTime; + ULARGE_INTEGER u; + u.QuadPart = Time; + u.QuadPart = u.QuadPart * FILETIME_CHUNKS_PER_SECOND + time_t_epoch_base_filetime.QuadPart; - if (pTM == NULL) - return FALSE; - - SystemTime.wYear = pTM->tm_year + 1900; - SystemTime.wMonth = pTM->tm_mon + 1; - SystemTime.wDay = pTM->tm_mday; - SystemTime.wHour = pTM->tm_hour; - SystemTime.wMinute = pTM->tm_min; - SystemTime.wSecond = pTM->tm_sec; - SystemTime.wMilliseconds = 0; + pFileTime->dwLowDateTime = u.LowPart; + pFileTime->dwHighDateTime = u.HighPart; - return SystemTimeToFileTime(&SystemTime, &LocalTime) && - LocalFileTimeToFileTime(&LocalTime, pFileTime); + return TRUE; } DllExport int @@ -2220,38 +2218,38 @@ win32_utime(const char *filename, struct utimbuf *times) { dTHX; HANDLE handle; - FILETIME ftCreate; FILETIME ftAccess; FILETIME ftWrite; struct utimbuf TimeBuffer; - int rc; + int rc = -1; filename = PerlDir_mapA(filename); - rc = utime(filename, times); - - /* EACCES: path specifies directory or readonly file */ - if (rc == 0 || errno != EACCES) - return rc; - - if (times == NULL) { - times = &TimeBuffer; - time(×->actime); - times->modtime = times->actime; - } - /* This will (and should) still fail on readonly files */ handle = CreateFileA(filename, GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (handle == INVALID_HANDLE_VALUE) - return rc; + if (handle == INVALID_HANDLE_VALUE) { + translate_to_errno(); + return -1; + } - if (GetFileTime(handle, &ftCreate, &ftAccess, &ftWrite) && - filetime_from_time(&ftAccess, times->actime) && - filetime_from_time(&ftWrite, times->modtime) && - SetFileTime(handle, &ftCreate, &ftAccess, &ftWrite)) - { - rc = 0; + if (times == NULL) { + times = &TimeBuffer; + time(×->actime); + times->modtime = times->actime; + } + + if (filetime_from_time(&ftAccess, times->actime) && + filetime_from_time(&ftWrite, times->modtime)) { + if (SetFileTime(handle, NULL, &ftAccess, &ftWrite)) { + rc = 0; + } + else { + translate_to_errno(); + } + } + else { + errno = EINVAL; /* bad time? */ } CloseHandle(handle); @@ -3195,39 +3193,9 @@ win32_abort(void) DllExport int win32_fstat(int fd, Stat_t *sbufptr) { - int result; - struct _stati64 st; - dTHX; - result = _fstati64(fd, &st); - if (result == 0) { - sbufptr->st_mode = st.st_mode; - sbufptr->st_uid = st.st_uid; - sbufptr->st_gid = st.st_gid; - sbufptr->st_rdev = st.st_rdev; - sbufptr->st_size = st.st_size; - sbufptr->st_atime = st.st_atime; - sbufptr->st_mtime = st.st_mtime; - sbufptr->st_ctime = st.st_ctime; - - if (w32_sloppystat) { - sbufptr->st_nlink = st.st_nlink; - sbufptr->st_dev = st.st_dev; - sbufptr->st_ino = st.st_ino; - } - else { - HANDLE handle = (HANDLE)win32_get_osfhandle(fd); - BY_HANDLE_FILE_INFORMATION bhi; - if (GetFileInformationByHandle(handle, &bhi)) { - sbufptr->st_nlink = bhi.nNumberOfLinks; - sbufptr->st_ino = bhi.nFileIndexHigh; - sbufptr->st_ino <<= 32; - sbufptr->st_ino |= bhi.nFileIndexLow; - sbufptr->st_dev = bhi.dwVolumeSerialNumber; - } - } - } - - return result; + HANDLE handle = (HANDLE)win32_get_osfhandle(fd); + + return win32_stat_low(handle, NULL, 0, sbufptr); } DllExport int @@ -4818,6 +4786,17 @@ Perl_win32_init(int *argcp, char ***argvp) } } #endif + + { + FILETIME ft; + if (!SystemTimeToFileTime(&time_t_epoch_base_systemtime, + &ft)) { + fprintf(stderr, "panic: cannot convert base system time to filetime\n"); /* no interp */ + exit(1); + } + time_t_epoch_base_filetime.LowPart = ft.dwLowDateTime; + time_t_epoch_base_filetime.HighPart = ft.dwHighDateTime; + } } void |