1#!./perl
2
3BEGIN {
4    chdir 't' if -d 't';
5    @INC = '../lib';
6    require "./test.pl";
7}
8
9use strict;
10use Fcntl ":seek";
11use Config;
12use Errno;
13use Cwd "getcwd";
14
15Win32::FsType() eq 'NTFS'
16    or skip_all("need NTFS");
17
18my (undef, $maj, $min) = Win32::GetOSVersion();
19
20my $vista_or_later = $maj >= 6;
21
22my $tmpfile1 = tempfile();
23my $tmpfile2 = tempfile();
24
25# test some of the win32 specific stat code, since we
26# don't depend on the CRT for some of it
27
28ok(link($0, $tmpfile1), "make a link to test nlink");
29
30my @st = stat $0;
31open my $fh, "<", $0 or die;
32my @fst = stat $fh;
33
34ok(seek($fh, 0, SEEK_END), "seek to end");
35my $size = tell($fh);
36close $fh;
37
38# the ucrt stat() is inconsistent here, using an A=0 drive letter for stat()
39# and the fd for fstat(), I assume that's something backward compatible.
40#
41# I don't see anything we could reasonable populate it with either.
42$st[6] = $fst[6] = 0;
43
44is("@st", "@fst", "check named stat vs handle stat");
45
46ok($st[0], "we set dev by default now");
47ok($st[1], "and ino");
48
49# unlikely, but someone else might have linked to win32/stat.t
50cmp_ok($st[3], '>', 1, "should be more than one link");
51
52# we now populate all stat fields ourselves, so check what we can
53is($st[7], $size, "we fetch size correctly");
54
55cmp_ok($st[9], '<=', time(), "modification time before or on now");
56ok(-f $0, "yes, we are a file");
57ok(-d "win32", "and win32 is a directory");
58pipe(my ($p1, $p2));
59ok(-p $p1, "a pipe is a pipe");
60close $p1; close $p2;
61ok(-r $0, "we are readable");
62ok(!-x $0, "but not executable");
63ok(-e $0, "we exist");
64
65ok(open(my $nul, ">", "nul"), "open nul");
66ok(-c $nul, "nul is a character device");
67close $nul;
68
69my $nlink = $st[3];
70
71# check we get nlinks etc for a directory
72@st = stat("win32");
73ok($st[0], "got dev for a directory");
74ok($st[1], "got ino for a directory");
75ok($st[3], "got nlink for a directory");
76
77# symbolic links
78unlink($tmpfile1); # no more hard link
79
80if (open my $fh, ">", "$tmpfile1.bat") {
81    ok(-x "$tmpfile1.bat", 'batch file is "executable"');
82    SKIP: {
83        skip "executable bit for handles needs vista or later", 1
84            unless $vista_or_later;
85        ok(-x $fh, 'batch file handle is "executable"');
86    }
87    close $fh;
88    unlink "$tmpfile1.bat";
89}
90
91# mklink is available from Vista onwards
92# this may only work in an admin shell
93# MKLINK [[/D] | [/H] | [/J]] Link Target
94if (system("mklink $tmpfile1 win32\\stat.t") == 0) {
95    ok(-l $tmpfile1, "lstat sees a symlink");
96
97    # check stat on file vs symlink
98    @st = stat $0;
99    my @lst = stat $tmpfile1;
100
101    $st[6] = $lst[6] = 0;
102
103    is("@st", "@lst", "check stat on file vs link");
104
105    # our hard link no longer exists, check that is reflected in nlink
106    is($st[3], $nlink-1, "check nlink updated");
107
108    is((lstat($tmpfile1))[7], length(readlink($tmpfile1)),
109       "check size matches length of link");
110
111    unlink($tmpfile1);
112}
113
114# similarly for a directory
115if (system("mklink /d $tmpfile1 win32") == 0) {
116    ok(-l $tmpfile1, "lstat sees a symlink on the directory symlink");
117
118    # check stat on directory vs symlink
119    @st = stat "win32";
120    my @lst = stat $tmpfile1;
121
122    $st[6] = $lst[6] = 0;
123
124    is("@st", "@lst", "check stat on dir vs link");
125
126    # for now at least, we need to rmdir symlinks to directories
127    rmdir( $tmpfile1 );
128}
129
130# check a junction looks like a symlink
131
132if (system("mklink /j $tmpfile1 win32") == 0) {
133    ok(-l $tmpfile1, "lstat sees a symlink on the directory junction");
134
135    my @st = lstat($tmpfile1);
136    is($st[7], length(readlink($tmpfile1)),
137       "check returned length matches POSIX");
138
139    rmdir( $tmpfile1 );
140}
141
142# test interaction between stat and utime
143if (ok(open(my $fh, ">", $tmpfile1), "make a work file")) {
144    # make our test file
145    close $fh;
146
147    my @st = stat $tmpfile1;
148    ok(@st, "stat our work file");
149
150    # switch to the other half of the year, to flip from/to daylight
151    # savings time.  It won't always do so, but it's close enough and
152    # avoids having to deal with working out exactly when it
153    # starts/ends (if it does), along with the hemisphere.
154    #
155    # By basing this on the current file times and using an offset
156    # that's the multiple of an hour we ensure the filesystem
157    # resolution supports the time we set.
158    my $moffset = 6 * 30 * 24 * 3600;
159    my $aoffset = $moffset - 24 * 3600;;
160    my $mymt = $st[9] - $moffset;
161    my $myat = $st[8] - $aoffset;
162    ok(utime($myat, $mymt, $tmpfile1), "set access and mod times");
163    my @mst = stat $tmpfile1;
164    ok(@mst, "fetch stat after utime");
165    is($mst[9], $mymt, "check mod time");
166    is($mst[8], $myat, "check access time");
167
168    unlink $tmpfile1;
169}
170
171# same for a directory
172if (ok(mkdir($tmpfile1), "make a work directory")) {
173    my @st = stat $tmpfile1;
174    ok(@st, "stat our work directory");
175
176    my $moffset = 6 * 30 * 24 * 3600;
177    my $aoffset = $moffset - 24 * 3600;;
178    my $mymt = $st[9] - $moffset;
179    my $myat = $st[8] - $aoffset;
180    ok(utime($myat, $mymt, $tmpfile1), "set access and mod times");
181    my @mst = stat $tmpfile1;
182    ok(@mst, "fetch stat after utime");
183    is($mst[9], $mymt, "check mod time");
184    is($mst[8], $myat, "check access time");
185
186    rmdir $tmpfile1;
187}
188
189 SKIP:
190{ # github 19668
191    $Config{ivsize} == 8
192        or skip "Need 64-bit int", 1;
193    open my $tmp, ">", $tmpfile1
194        or skip "Cannot create test file: $!", 1;
195    close $tmp;
196    fresh_perl_is("utime(500_000_000_000, 500_000_000_000, '$tmpfile1')",
197                  "", { stderr => 1 },
198                  "check debug output removed");
199    unlink $tmpfile1;
200}
201
202# Other stat issues possibly fixed by the stat() re-work
203
204# https://github.com/Perl/perl5/issues/9025 - win32 - file test operators don't work for //?/UNC/server/file filenames
205# can't really make a reliable regression test for this
206# reproduced original problem with a gcc build
207# confirmed fixed with a gcc build
208
209# https://github.com/Perl/perl5/issues/8502 - filetest problem with STDIN/OUT on Windows
210
211{
212    ok(-r *STDIN, "check stdin is readable");
213    ok(-w *STDOUT, "check stdout is writable");
214
215    # CompareObjectHandles() could fix this, but requires Windows 10
216    local our $TODO = "dupped *STDIN and *STDOUT not read/write";
217    open my $dupin, "<&STDIN" or die;
218    open my $dupout, ">&STDOUT" or die;
219    ok(-r $dupin, "check duplicated stdin is readable");
220    ok(-w $dupout, "check duplicated stdout is writable");
221}
222
223# https://github.com/Perl/perl5/issues/6080 - Last mod time from stat() can be wrong on Windows NT/2000/XP
224# tested already
225
226# https://github.com/Perl/perl5/issues/4145 - Problem with filetest -x _ on Win2k AS Perl build 626
227# tested already
228
229# https://github.com/Perl/perl5/issues/14687 -  Function lstat behavior case differs between Windows and Unix #14687
230
231{
232    local our $TODO = "... .... treated as .. by Win32 API";
233    ok(!-e ".....", "non-existing many dots shouldn't returned existence");
234}
235
236# https://github.com/Perl/perl5/issues/7410 - -e tests not reliable under Win32
237{
238    # there's to issues here:
239    # 1) CreateFile() successfully opens " . . " when opened with backup
240    # semantics/directory
241    # 2) opendir(" . . ") becomes FindFirstFile(" . . /*") which fails
242    #
243    # So we end up with success for the first and failure for the second,
244    # making them inconsistent, there may be a Vista level fix for this,
245    # but if we expect -e " . . " to fail we need a more complex fix.
246    local our $TODO = "strange space handling by Windows";
247    ok(!-e " ", "filename ' ' shouldn't exist");
248    ok(!-e " . . ", "filename ' . . ' shouldn't exist");
249    ok(!-e " .. ", "filename ' .. ' shouldn't exist");
250    ok(!-e " . ", "filename ' . ' shouldn't exist");
251
252    ok(!!-e " . . " == !!opendir(FOO, " . . "),
253       "these should be consistent");
254}
255
256# https://github.com/Perl/perl5/issues/12431 - Win32: -e '"' always returns true
257
258{
259    ok(!-e '"', qq(filename '"' shouldn't exist));
260}
261
262# https://github.com/Perl/perl5/issues/20204
263# Win32: stat/unlink fails on UNIX sockets
264SKIP:
265{
266    use IO::Socket;
267    unlink $tmpfile1;
268    my $listen = IO::Socket::UNIX->new(Local => $tmpfile1, Listen => 0)
269        or skip "Cannot create unix socket", 1;
270    ok(-S $tmpfile1, "can stat a socket");
271    ok(!-l $tmpfile1, "doesn't look like a symlink");
272    unlink $tmpfile2;
273    if (system("mklink $tmpfile2 $tmpfile1") == 0) {
274        ok(-l $tmpfile2, "symlink to socket is a symlink (via lstat)");
275        ok(-S $tmpfile2, "symlink to socket is also a socket (via stat)");
276        unlink $tmpfile2;
277    }
278    close $listen;
279    unlink $tmpfile1;
280}
281
282{
283    # if a symlink chain leads to a socket, or loops, or is broken,
284    # CreateFileA() fails, so we do our own link following.
285    # The link leading to a socket is checked above, here check loops
286    # fail, and that we get ELOOP (which isn't what MSVC returns, but
287    # try to be better).
288    if (system("mklink $tmpfile1 $tmpfile2") == 0
289        && system("mklink $tmpfile2 $tmpfile1") == 0) {
290        ok(!stat($tmpfile1), "looping symlink chain fails stat");
291        is($!+0, &Errno::ELOOP, "check error set");
292        ok(lstat($tmpfile1), "looping symlink chain passes lstat");
293
294        unlink $tmpfile2;
295        ok(!stat($tmpfile1), "broken symlink");
296        is($!+0, &Errno::ENOENT, "check error set");
297        ok(lstat($tmpfile1), "broken symlink chain passes lstat");
298    }
299    unlink $tmpfile1, $tmpfile2;
300}
301
302{
303    # $tmpfile4 -> $tmpfile1/file1 -> ../$tmpfile2 -> abspath($tmpfile3)
304    # $tmpfile3 either doesn't exist, is a file, or is a socket
305    my ($tmpfile3, $tmpfile4) = (tempfile(), tempfile());
306    ok(mkdir($tmpfile1), "make a directory");
307    my $cwd = getcwd();
308    if (system(qq(mklink $tmpfile4 $tmpfile1\\file1)) == 0
309        && system(qq(mklink $tmpfile1\\file1 ..\\$tmpfile2)) == 0
310        && system(qq(mklink $tmpfile2 "$cwd\\$tmpfile3")) == 0) {
311        ok(-l $tmpfile4, "yes, $tmpfile4 is a symlink");
312        ok(!-e $tmpfile4, "but we can't stat it");
313
314        open my $fh, ">", $tmpfile3 or die $!;
315        close $fh;
316        ok(-f $tmpfile4, "now $tmpfile4 leads to a file");
317        unlink $tmpfile3;
318
319      SKIP:
320        {
321            my $listen = IO::Socket::UNIX->new(Local => $tmpfile3, Listen => 0)
322                or skip "Cannot create unix socket", 1;
323            ok(!-f $tmpfile4, "$tmpfile4 no longer leads to a file");
324            ok(-S $tmpfile4, "now $tmpfile4 leads to a socket");
325            ok(-S "$tmpfile1/file1", "$tmpfile1/file1 should lead to a socket");
326            ok(-S $tmpfile2, "$tmpfile2 should lead to a socket");
327            unlink $tmpfile3;
328        }
329    }
330    unlink $tmpfile2, $tmpfile4, "$tmpfile1/file1";
331    rmdir $tmpfile1;
332}
333done_testing();
334