#=============================================================================== # # lib/Win32/UTCFileTime.pm # # DESCRIPTION # Module providing functions to get/set UTC file times with stat/utime on # Win32. # # COPYRIGHT # Copyright (C) 2003-2008, 2012-2014 Steve Hay. All rights reserved. # # LICENCE # You may distribute under the terms of either the GNU General Public License # or the Artistic License, as specified in the LICENCE file. # #=============================================================================== package Win32::UTCFileTime; use 5.008001; use strict; use warnings; use Carp qw(croak); use Exporter qw(); use XSLoader qw(); sub stat(;$); sub lstat(;$); sub alt_stat(;$); sub utime(@); sub _fixup_file($); #=============================================================================== # MODULE INITIALIZATION #=============================================================================== our(@ISA, @EXPORT, @EXPORT_OK, $VERSION); BEGIN { @ISA = qw(Exporter); @EXPORT = qw( stat lstat utime ); @EXPORT_OK = qw( $ErrStr alt_stat ); $VERSION = '1.58'; XSLoader::load(__PACKAGE__, $VERSION); } # Last error message. our $ErrStr = ''; # Control whether to try alt_stat() if CORE::stat() or CORE::lstat() fails. # (Boolean.) our $Try_Alt_Stat = 0; #=============================================================================== # PUBLIC API #=============================================================================== # Autoload the SEM_* flags from the constant() XS function. sub AUTOLOAD { our $AUTOLOAD; # Get the name of the constant to generate a subroutine for. (my $constant = $AUTOLOAD) =~ s/^.*:://o; # Avoid deep recursion on AUTOLOAD() if constant() is not defined. croak('Unexpected error in AUTOLOAD(): constant() is not defined') if $constant eq 'constant'; my($error, $value) = constant($constant); # Handle any error from looking up the constant. croak($error) if $error; # Generate an in-line subroutine returning the required value. { no strict 'refs'; *$AUTOLOAD = sub { return $value }; } # Switch to the subroutine that we have just generated. goto &$AUTOLOAD; } # Specialized import() method to handle the ':globally' pseudo-symbol. # This method is based on the import() method in the standard library module # File::Glob (version 1.23). sub import { # If the ':globally' pseudo-symbol is found in the list of symbols to export # then export stat(), lstat() and utime() to the special CORE::GLOBAL # package. my @args = grep { my $passthrough; if ($_ eq ':globally') { no warnings 'redefine'; *CORE::GLOBAL::stat = \&Win32::UTCFileTime::stat; *CORE::GLOBAL::lstat = \&Win32::UTCFileTime::lstat; *CORE::GLOBAL::utime = \&Win32::UTCFileTime::utime; } else { $passthrough = 1; } $passthrough; } @_; # Don't call Exporter's import() if there are no more arguments left than # our own module name: import() is not called for "use MODULE ()" so we # should not continue if we are now in that case. return if scalar @_ > 1 and scalar @args == 1; local $Exporter::ExportLevel = $Exporter::ExportLevel + 1; Exporter::import(@args); } sub stat(;$) { my $file = @_ ? shift : $_; $ErrStr = ''; $file = _fixup_file($file); # Make sure we do not display a message box asking the user to insert a # floppy disk or CD-ROM. my $old_umode = _set_error_mode(SEM_FAILCRITICALERRORS()); if (wantarray) { my @stats = CORE::stat $file; unless (@stats) { _set_error_mode($old_umode); if ($Try_Alt_Stat) { goto &alt_stat; } else { $ErrStr = "Can't stat file '$file': $!"; return; } } unless (@stats[8 .. 10] = _get_utc_file_times($file)) { _set_error_mode($old_umode); return; } _set_error_mode($old_umode); return @stats; } else { my $ret = CORE::stat $file; unless ($ret) { _set_error_mode($old_umode); if ($Try_Alt_Stat) { goto &alt_stat; } else { $ErrStr = "Can't stat file '$file': $!"; return $ret; } } _set_error_mode($old_umode); return $ret; } } sub lstat(;$) { my $link = @_ ? shift : $_; $ErrStr = ''; $link = _fixup_file($link); # Make sure we do not display a message box asking the user to insert a # floppy disk or CD-ROM. my $old_umode = _set_error_mode(SEM_FAILCRITICALERRORS()); if (wantarray) { my @lstats = CORE::lstat $link; unless (@lstats) { _set_error_mode($old_umode); if ($Try_Alt_Stat) { goto &alt_stat; } else { $ErrStr = "Can't stat link '$link': $!"; return; } } unless (@lstats[8 .. 10] = _get_utc_file_times($link)) { _set_error_mode($old_umode); return; } _set_error_mode($old_umode); return @lstats; } else { my $ret = CORE::lstat $link; unless ($ret) { _set_error_mode($old_umode); if ($Try_Alt_Stat) { goto &alt_stat; } else { $ErrStr = "Can't stat link '$link': $!"; return $ret; } } _set_error_mode($old_umode); return $ret; } } sub alt_stat(;$) { my $file = @_ ? shift : $_; $ErrStr = ''; $file = _fixup_file($file); # Make sure we do not display a message box asking the user to insert a # floppy disk or CD-ROM. my $old_umode = _set_error_mode(SEM_FAILCRITICALERRORS()); if (wantarray) { my @stats = _alt_stat($file); _set_error_mode($old_umode); return @stats; } else { my $ret = _alt_stat($file); _set_error_mode($old_umode); return $ret; } } sub utime(@) { my($atime, $mtime, @files) = @_; $ErrStr = ''; my $time = time; $atime = $time unless defined $atime; $mtime = $time unless defined $mtime; my $count = 0; foreach my $file (@files) { _set_utc_file_times($file, $atime, $mtime) and $count++; } return $count; } #=============================================================================== # PRIVATE API #=============================================================================== # This function is based on code taken from the win32_stat() function in Perl # (version 5.19.10). sub _fixup_file($) { my $file = shift; # Remove trailing slashes or backslashes. $file =~ s/[\/\\]+$//o; # Add a backslash if we only have a drive letter. $file .= '\\' if $file =~ /^[a-z]:$/io; return $file; } 1; __END__ #=============================================================================== # DOCUMENTATION #=============================================================================== =head1 NAME Win32::UTCFileTime - Get/set UTC file times with stat/utime on Win32 =head1 SYNOPSIS # Override built-in stat()/lstat()/utime() within current package only: use Win32::UTCFileTime qw(:DEFAULT $ErrStr); @stats = stat $file or die "stat() failed: $ErrStr\n"; $now = time; utime $now, $now, $file; # Or, override built-in stat()/lstat()/utime() within all packages: use Win32::UTCFileTime qw(:globally); ... # Use an alternative implementation of stat() instead: use Win32::UTCFileTime qw(alt_stat $ErrStr); @stats = alt_stat($file) or die "alt_stat() failed: $ErrStr\n"; =head1 DESCRIPTION This module provides replacements for Perl's built-in C and C functions that respectively get and set "correct" UTC file times instead of the erroneous values read and written by Microsoft's implementation of C and C, which Perl's built-in functions inherit on Win32 when built with the Microsoft C library. For completeness, a replacement for Perl's built-in C function is also provided, although in practice that is unimplemented on Win32 and just calls C anyway. (Note, however, that it calls the I C, not the override provided by this module, so you must use the C override provided by this module if you want "correct" UTC file times from C.) The problem with Microsoft's C and C, and hence Perl's built-in C, C and C when built with the Microsoft C library, is basically this: file times reported by C or stored by C may change by an hour as we move into or out of daylight saving time (DST) if the computer is set to "Automatically adjust clock for daylight saving changes" (which is the default setting) and the file is stored on an NTFS volume (which is the preferred filesystem used by Windows NT/2000/XP/2003). It seems particularly ironic that the problem should afflict the NTFS filesystem because the C values used by both C and C express UTC-based times, and NTFS stores file times in UTC. However, Microsoft's implementation of both of these functions use a variety of Win32 API calls that mangle the numbers in ways that do not quite turn out right when a DST season change is involved. On FAT volumes, the filesystem used by Windows 95/98/ME, file times are stored in local time and are put through even more contortions by these functions, but actually emerge correctly, so file times are stable across DST seasons on FAT volumes. The NTFS/FAT difference is taken into account by this module's replacement C, C and C functions so that corrections are not erroneously applied when they should not be. The problems that arise when mangling time values between UTC and local time are because the mapping from UTC to local time is not one-to-one. It is straightforward to convert UTC to local time, but there is an ambiguity when converting back from local time to UTC involving DST. The Win32 API provides two documented functions (C and C) for these conversions that resolve the ambiguity by, arguably "wrongly", using an algorithm involving the current system time rather than the file time being converted to decide whether to apply a DST correction; the advantage of this scheme is that these functions are exact inverses. Another, undocumented, function is also used internally by C for the tricky local time to UTC conversion, which, "correctly", uses the file time being converted to decide whether to apply a DST correction. The standard C library provides C for UTC to local time conversion, albeit from C format to C format, (and also C for the same structure-conversion without converting to local time), and C for local time to UTC conversion, applying a DST correction or not as instructed by one of the fields in its C argument. Additionally, if you build perl with Visual Studio 2013 (VC12) then perl's C function will suffer from a new bug introduced into the C RTL DLL's C function, which Microsoft do not intend to fix until a future version of Visual Studio. See L<"BACKGROUND REFERENCE"> for more details. The replacement C and C functions provided by this module behave identically to Perl's built-in functions of the same name, except that: =over 4 =item * the last access time, last modification time and creation time return values are all "correct" UTC-based values, stable across DST seasons; =item * the argument (or C<$_> if no argument is given) must be a file (path or name), not a filehandle (and not the special filehandle consisting of an underscore character either). =back In fact, both of these replacement functions work by calling Perl's corresponding built-in function first and then overwriting the file time fields in the lists thus obtained with the corrected values. In this way, all of the extra things done by Perl's built-in functions besides simply calling the underlying C C function are inherited by this module. (The replacement functions also incorporate one slight improvement to the built-in functions that was introduced in Perl 5.8.9 (namely, that they work correctly on directories specified with trailing slashes or backslashes), thus making this improvement available even when using an older version of Perl.) The replacement C function provided by this module behaves identically to Perl's built-in function of the same name, except that: =over 4 =item * the last access time and last modification time arguments are both "correctly" interpreted as UTC-based values, stable across DST seasons; =item * no warnings about "Use of uninitialized value in utime" are produced when either file time argument is specified as C. =back In particular, the one extra thing done by Perl's built-in function besides simply calling the underlying C C function (namely, providing a fix so that it works on directories as well as files) is also incorporated into this module's replacement function. All three replacement functions are exported to the caller's package by default. A special C<:globally> export pseudo-symbol is also provided that will export all three functions to the CORE::GLOBAL package, which effectively overrides the Perl built-in functions in I packages, not just the caller's. The C function is only exported when explicitly requested. =head2 Functions =over 4 =item C Gets the status information for the file $file. If $file is omitted then C<$_> is used instead. In list context, returns the same 13-element list as Perl's built-in C function on success, or returns an empty list and sets $ErrStr on failure. For convenience, here are the members of that 13-element list and their meanings on Win32: 0 dev drive number of the disk containing the file (same as rdev) 1 ino not meaningful on Win32; always returned as 0 2 mode file mode (type and permissions) 3 nlink number of (hard) links to the file; always 1 on non-NTFS drives 4 uid numeric user ID of file's owner; always 0 on Win32 5 gid numeric group ID of file's owner; always 0 on Win32 6 rdev drive number of the disk containing the file (same as dev) 7 size total size of file, in bytes 8 atime last access time in seconds since the epoch 9 mtime last modification time in seconds since the epoch 10 ctime creation time in seconds since the epoch 11 blksize not implemented on Win32; returned as '' 12 blocks not implemented on Win32; returned as '' where the epoch was at 00:00:00 Jan 01 1970 UTC and the drive number of the disk is 0 for F, 1 for F, 2 for F and so on. Because the mode contains both the file type (the C bit is set if $file specifies a directory; the C bit is set if the $file specifies a regular file) and its permissions (the user read/write bits are set according to the file's permission mode; the user execute bits are set according to the filename extension), you should mask off the file type portion and C using a C<"%04o"> if you want to see the real permissions: $mode = (stat($filename))[2]; printf "Permissions are %04o\n", $mode & 07777; You can also import symbolic mode constants (C) and functions (C) from the Fcntl module to assist in examining the mode. See L for more details. Note that you cannot use this module in conjunction with the File::stat module (which provides a convenient, by-name, access mechanism to the fields of the 13-element list) because both modules operate by overriding Perl's built-in C function. Only the second override to be applied would have effect. In scalar context, returns a Boolean value indicating success or failure (and sets $ErrStr on failure). =item C Gets the status information for the symbolic link $link. If $file is omitted then C<$_> is used instead. This is the same as C on Win32, which does not implement symbolic links. =item C Gets the status information for the file $file. If $file is omitted then C<$_> is used instead. Behaves almost identically to C above, but uses this module's own implementation of the standard C library C function that can succeed in some cases where Microsoft's implementation fails. Microsoft's C, and hence Perl's built-in C and the replacement C function above, calls the Win32 API function C. That function is used to search a directory for a file, and thus requires the process to have "List Folder Contents" permission on the directory containing the $file in question. If that permission is denied then C will fail. C avoids this problem by using a different Win32 API function, C, instead. That function opens a file directly and hence does not require the process to have "List Folder Contents" permission on the parent directory. B field correctly;> the other fields will be set to zero, like the Perl built-in C and C functions do for directories on which C fails (which can also happen in that case, e.g. on sharenames). The main disadvantage with using this function is that the entire C has to be built by hand by it, rather than simply inheriting most of it from the Microsoft C call and then overwriting the file time fields. Thus, some of the fields, notably the C field, which is somewhat ambiguous on Win32, may have different values to those that would have been set by the other C functions. =item C Sets the last access time and last modification time to the values specified by $atime and $mtime respectively for each of the files in @files. The process must have write access to each of the files concerned in order to change these file times. Returns the number of files successfully changed and sets $ErrStr if one or more files could not be changed. The times should both be specified as the number of seconds since the epoch, where the epoch was at 00:00:00 Jan 01 1970 UTC. If the undefined value is used for either file time argument then the current time will be used for that value. Note that the 11th element of the 13-element list returned by C is the creation time on Win32, not the inode change time as it is on many other operating systems. Therefore, neither Perl's built-in C function nor this replacement function set that value to the current time as would happen on other operating systems. =back =head2 Variables =over 4 =item $ErrStr Last error message. If any function fails then a description of the last error will be set in this variable for use in reporting the cause of the failure, much like the use of the Perl Special Variables C<$!> and C<$^E> after failed system calls and Win32 API calls. Note that C<$!> and/or C<$^E> may also be set on failure, but this is not always the case so it is better to check $ErrStr instead. Any relevant messages from C<$!> or C<$^E> will form part of the message in $ErrStr anyway. See L<"Error Values"> for a listing of the possible values of $ErrStr. If a function succeeds then this variable will be set to the null string. =item $Try_Alt_Stat Control whether to try C if C or C fails. Boolean value. As documented in the L<"DESCRIPTION"> section above, the replacement C and C functions each call their built-in counterparts first and then overwrite the file time fields in the lists thus obtained with the corrected values. Setting this variable to a true value will cause the replacement functions to switch to C (via a C call) if the C or C call fails. The default value is 0, i.e. the C function is not tried. =back =head1 DIAGNOSTICS =head2 Warnings and Error Messages This module may produce the following diagnostic messages. They are classified as follows (a la L): (W) A warning (optional). (F) A fatal error (trappable). (I) An internal error that you should never see (trappable). =over 4 =item Can't close file descriptor '%d' for file '%s' after updating: %s (W) The specified file descriptor for the specified file could not be closed after updating the file times using it. The system error message corresponding to the standard C library C variable is also given. =item Can't close file object handle '%lu' for file '%s' after reading: %s (W) The specified file object handle for the specified file could not be closed after reading file information from it. The system error message corresponding to the Win32 API last error code is also given. =item Can't close file object handle '%lu' for file '%s' after updating: %s (W) The specified file object handle for the specified file could not be closed after updating the file times using it. The system error message corresponding to the Win32 API last error code is also given. =item Can't convert base SYSTEMTIME to FILETIME: %s (I) The C representation of the epoch of C values (namely, 00:00:00 Jan 01 1970 UTC) could not be converted to its C representation. The system error message corresponding to the Win32 API last error code is also given. =item Can't determine operating system platform: %s. Assuming the platform is Windows NT (W) The operating system platform (i.e. Win32s, Windows (95/98/ME), Windows NT or Windows CE) could not be determined. This information is used by the C function to decide whether a F<".cmd"> file extension represents an "executable file" when setting up the C field of the C. A Windows NT platform is assumed in this case. The system error message corresponding to the Win32 API last error code is also given. =item %s is not a valid Win32::UTCFileTime macro (F) You attempted to lookup the value of the specified constant in the Win32::UTCFileTime module, but that constant is unknown to this module. =item Overflow: Too many links (%lu) to file '%s' (W) The number of hard links to the specified file is greater than the largest C, and therefore cannot be assigned to the C field of the C set-up by C. The largest C itself is used instead in this case. =item Unexpected error in AUTOLOAD(): constant() is not defined (I) There was an unexpected error looking up the value of the specified constant: the constant-lookup function itself is apparently not defined. =item Unexpected return type %d while processing Win32::UTCFileTime macro %s (I) There was an unexpected error looking up the value of the specified constant: the C component of the constant-lookup function returned an unknown type. =item Your vendor has not defined Win32::UTCFileTime macro %s (I) You attempted to lookup the value of the specified constant in the Win32::UTCFileTime module, but that constant is apparently not defined. =back =head2 Error Values Each function sets $ErrStr to a value indicating the cause of the error when they fail. The possible values are as follows: =over 4 =item Can't get file information for file '%s': %s File information could not be read from an open file handle on the specified file. The system error message corresponding to the Win32 API last error code is also given. =item Can't get file object handle after opening file '%s' for updating as file descriptor '%d': %s", A file object handle could not be obtained for the specified file descriptor, which the specified file has been opened as for updating. The system error message corresponding to the standard C library C variable is also given. =item Can't open file '%s' for reading: %s The specified file could not be opened for reading the file information. The system error message corresponding to the Win32 API last error code is also given. =item Can't open file '%s' for updating: %s The specified file could not be opened for updating the file times. The system error message corresponding to the Win32 API last error code is also given. =item Can't set file times for file '%s': %s The file times could not be updated on an open file handle on the specified file. The system error message corresponding to the Win32 API last error code is also given. =item Can't stat %s '%s': %s The C or C function failed for the specified file or link. (The replacement C and C functions call their CORE counterparts before getting the "correct" UTC file times.) The system error message corresponding to the standard C library C variable is also given. =item Wildcard in filename '%s' The specified filename passed to the C function contains a DOS-style wildcard character, namely "?" or "*". The function is not able to handle filenames of this format. =back In some cases, the functions may also leave the Perl Special Variables C<$!> and/or C<$^E> set to values indicating the cause of the error when they fail; one or the other of these will be incorporated into the $ErrStr message in such cases, as indicated above. The possible values of each are as follows (C<$!> shown first, C<$^E> underneath): =over 4 =item C (Permission denied) =item C (Access is denied) [C only.] One or more of the @files is read-only. (The process must have write access to each file to be able to update its file times.) =item C (Too many open files) =item C (The system cannot open the file) The maximum number of file descriptors has been reached. (Each file must be opened in order to read file information from it or to update its file times.) =item C (No such file or directory) =item C (The system cannot find the file specified) The filename or path in $file was not found. =back Note that since each function uses Win32 API functions rather than standard C library functions, they will probably only set C<$^E> (which represents the Win32 API last error code, as returned by C), not C<$!> (which represents the standard C library C variable). Other values may also be produced by various functions that are used within this module whose possible error codes are not documented. See L|perlvar/$!>, L|perlvar/%!>, L|perlvar/$^E> and L in L, C and C in L, and L and L for details on how to check these values. =head1 BACKGROUND REFERENCE A number of Microsoft Knowledge Base articles refer to the odd characteristics of the Win32 API functions and the Microsoft C library functions involved, in particular see: =over 4 =item * 128126: FileTimeToLocalFileTime() Adjusts for Daylight Saving Time =item * 129574: Time Stamp Changes with Daylight Savings =item * 158588: Obtaining Universal Coordinated Time (UTC) from NTFS Files =item * 190315: Some CRT File Functions Adjust For Daylight Savings Time =back As these articles themselves emphasize, the behaviour in question is by design, not a bug. As an aside, another Microsoft Knowledge Base article (214661: FIX: Daylight Savings Time Bug in C Run-Time Library) refers to a different problem involving the Microsoft C library that was confirmed as a bug and was fixed in Visual C++ 6.0 Service Pack 3, so it is worth ensuring that your edition of Visual C++ is upgraded to at least that Service Pack level when you build perl and this module. (At the time of writing, Service Pack 6 is the latest available for Visual C++ 6.0.) An excellent overview of the problem with Microsoft's C was written by Jonathan M Gilligan and posted on the Code Project website (F). He has kindly granted permission to use his article here to describe the problem more fully. A slightly edited version of it now follows; the original article can be found at the URL F. (The article was accompanied by a C library, adapted from code written for CVSNT (F) by Jonathan and Tony M Hoyle, which implemented the solution outlined at the end of his article. The solution provided by this module is partly based on that library and the original CVSNT code itself (version 2.0.4), which both authors kindly granted permission to use under the terms of the Perl Artistic License as well as the GNU GPL.) =head2 Introduction Not many Windows developers seem aware of it, but Microsoft deliberately designed Windows NT to report incorrect file creation, modification, and access times. This decision is documented in the Knowledge Base in articles Q128126 and Q158588. For most purposes, this behaviour is innocuous, but as Microsoft writes in Q158588, After the automatic correction for Daylight Savings Time, monitoring programs comparing current time/date stamps to reference data that were not written using Win32 API calls which directly obtain/adjust to Universal Coordinated Time (UTC) will erroneously report time/date changes on files. Programs affected by this issue may include version-control software, database-synchronization software, software-distribution packages, backup software... This behaviour is responsible for a flood of questions to the various support lists for CVS, following the first Sunday in April and the last Sunday in October, with scores of people complaining that CVS now reports erroneously that their files have been modified. This is commonly known as the "red file bug" because the WinCVS shell uses red icons to indicate modified files. Over the past two years, several people have made concerted efforts to fix this bug and determine the correct file modification times for files both on NTFS and FAT volumes. It has proved surprisingly difficult to solve this problem correctly. I believe that I have finally gotten everything right and would like to share my solution with anyone else who cares about this issue. =head2 An example of the problem Run the following batch file on a computer where F is an NTFS volume and F is a FAT-formatted floppy disk. You will need write access to F and F. This script will change your system time and date, so be prepared to manually restore them afterwards. REM Test_DST_Bug.bat REM File Modification Time Test Date /T Time /T Date 10/27/2001 Time 10:00 AM Echo Foo > A:\Foo.txt Time 10:30 AM Echo Foo > C:\Bar.txt dir A:\Foo.txt C:\Bar.txt Date 10/28/2001 dir A:\Foo.txt C:\Bar.txt REM Prompt the user to reset the date and time. date time The result looks something like this (abridged to save space) C:\>Date 10/27/2001 C:\>dir A:\Foo.txt C:\Bar.txt Directory of A:\ 10/27/01 10:00a 6 Foo.txt Directory of C:\ 10/27/01 10:30a 6 Bar.txt C:\>Date 10/28/2001 C:\>dir A:\Foo.txt C:\Bar.txt Directory of A:\ 10/27/01 10:00a 6 Foo.txt Directory of C:\ 10/27/01 09:30a 6 Bar.txt On 27 October, Windows correctly reports that F was modified half an hour after F, but the next day, Windows has changed its mind and decided that actually, F was modified half an hour I F. A naive programmer might think this was a bug, but as Microsoft emphasized, I =head2 Why Windows has this problem The origin of this file time problem lies in the early days of MS-DOS and PC-DOS. Unix and other operating systems designed for continuous use and network communications have long tended to store times in GMT (later UTC) format so computers in different time zones can accurately determine the order of different events. However, when Microsoft adapted DOS for the IBM PC, the personal computer was not envisioned in the context of wide-area networks, where it would be important to compare the modification times of files on the PC with those on another computer in another time zone. In the interest of efficiently using the very limited resources of the computer, Microsoft wisely decided not to waste bits or processor cycles worrying about time zones. To put this decision in context, recall that the first two generations of PCs did not have battery-backed real-time clocks, so you would generally put C and C