Release 960405
[wine] / files / dos_fs.c
1 /*
2  * DOS file system functions
3  *
4  * Copyright 1993 Erik Bos
5  * Copyright 1996 Alexandre Julliard
6  */
7
8 #include <sys/types.h>
9 #include <ctype.h>
10 #include <dirent.h>
11 #include <string.h>
12 #include <stdlib.h>
13 #include <stdio.h>
14 #include <sys/stat.h>
15 #include <time.h>
16 #if defined(__svr4__) || defined(_SCO_DS)
17 #include <sys/statfs.h>
18 #endif
19
20 #include "windows.h"
21 #include "dos_fs.h"
22 #include "drive.h"
23 #include "file.h"
24 #include "msdos.h"
25 #include "stddebug.h"
26 #include "debug.h"
27
28 /* Chars we don't want to see in DOS file names */
29 #define INVALID_DOS_CHARS  "*?<>|\"+=,;[] \345"
30
31 static const char *DOSFS_Devices[][2] =
32 {
33     { "CON",  "" },
34     { "PRN",  "" },
35     { "NUL",  "/dev/null" },
36     { "AUX",  "" },
37     { "LPT1", "" },
38     { "LPT2", "" },
39     { "LPT3", "" },
40     { "LPT4", "" },
41     { "COM1", "" },
42     { "COM2", "" },
43     { "COM3", "" },
44     { "COM4", "" }
45 };
46
47 #define GET_DRIVE(path) \
48     (((path)[1] == ':') ? toupper((path)[0]) - 'A' : DOSFS_CurDrive)
49
50     /* DOS extended error status */
51 WORD DOS_ExtendedError;
52 BYTE DOS_ErrorClass;
53 BYTE DOS_ErrorAction;
54 BYTE DOS_ErrorLocus;
55
56
57 /***********************************************************************
58  *           DOSFS_ValidDOSName
59  *
60  * Return 1 if Unix file 'name' is also a valid MS-DOS name
61  * (i.e. contains only valid DOS chars, lower-case only, fits in 8.3 format).
62  * File name can be terminated by '\0', '\\' or '/'.
63  */
64 static int DOSFS_ValidDOSName( const char *name )
65 {
66     static const char invalid_chars[] = INVALID_DOS_CHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
67     const char *p = name;
68     int len = 0;
69
70     if (*p == '.')
71     {
72         /* Check for "." and ".." */
73         p++;
74         if (*p == '.') p++;
75         /* All other names beginning with '.' are invalid */
76         return (IS_END_OF_NAME(*p));
77     }
78     while (!IS_END_OF_NAME(*p))
79     {
80         if (strchr( invalid_chars, *p )) return 0;  /* Invalid char */
81         if (*p == '.') break;  /* Start of the extension */
82         if (++len > 8) return 0;  /* Name too long */
83         p++;
84     }
85     if (*p != '.') return 1;  /* End of name */
86     p++;
87     if (IS_END_OF_NAME(*p)) return 0;  /* Empty extension not allowed */
88     len = 0;
89     while (!IS_END_OF_NAME(*p))
90     {
91         if (strchr( invalid_chars, *p )) return 0;  /* Invalid char */
92         if (*p == '.') return 0;  /* Second extension not allowed */
93         if (++len > 3) return 0;  /* Extension too long */
94         p++;
95     }
96     return 1;
97 }
98
99
100 /***********************************************************************
101  *           DOSFS_CheckDotDot
102  *
103  * Remove all '.' and '..' at the beginning of 'name'.
104  */
105 static const char * DOSFS_CheckDotDot( const char *name, char *buffer,
106                                        char sep , int *len )
107 {
108     char *p = buffer + strlen(buffer);
109
110     while (*name == '.')
111     {
112         if (IS_END_OF_NAME(name[1]))
113         {
114             name++;
115             while ((*name == '\\') || (*name == '/')) name++;
116         }
117         else if ((name[1] == '.') && IS_END_OF_NAME(name[2]))
118         {
119             name += 2;
120             while ((*name == '\\') || (*name == '/')) name++;
121             while ((p > buffer) && (*p != sep)) { p--; (*len)++; }
122             *p = '\0';  /* Remove trailing separator */
123         }
124         else break;
125     }
126     return name;
127 }
128
129
130 /***********************************************************************
131  *           DOSFS_ToDosFCBFormat
132  *
133  * Convert a file name to DOS FCB format (8+3 chars, padded with blanks),
134  * expanding wild cards and converting to upper-case in the process.
135  * File name can be terminated by '\0', '\\' or '/'.
136  * Return NULL if the name is not a valid DOS name.
137  */
138 const char *DOSFS_ToDosFCBFormat( const char *name )
139 {
140     static const char invalid_chars[] = INVALID_DOS_CHARS;
141     static char buffer[12];
142     const char *p = name;
143     int i;
144
145     /* Check for "." and ".." */
146     if (*p == '.')
147     {
148         p++;
149         strcpy( buffer, ".          " );
150         if (*p == '.') p++;
151         return (!*p || (*p == '/') || (*p == '\\')) ? buffer : NULL;
152     }
153
154     for (i = 0; i < 8; i++)
155     {
156         switch(*p)
157         {
158         case '\0':
159         case '\\':
160         case '/':
161         case '.':
162             buffer[i] = ' ';
163             break;
164         case '?':
165             p++;
166             /* fall through */
167         case '*':
168             buffer[i] = '?';
169             break;
170         default:
171             if (strchr( invalid_chars, *p )) return NULL;
172             buffer[i] = toupper(*p);
173             p++;
174             break;
175         }
176     }
177
178     if (*p == '*')
179     {
180         /* Skip all chars after wildcard up to first dot */
181         while (*p && (*p != '/') && (*p != '\\') && (*p != '.')) p++;
182     }
183     else
184     {
185         /* Check if name too long */
186         if (*p && (*p != '/') && (*p != '\\') && (*p != '.')) return NULL;
187     }
188     if (*p == '.') p++;  /* Skip dot */
189
190     for (i = 8; i < 11; i++)
191     {
192         switch(*p)
193         {
194         case '\0':
195         case '\\':
196         case '/':
197             buffer[i] = ' ';
198             break;
199         case '.':
200             return NULL;  /* Second extension not allowed */
201         case '?':
202             p++;
203             /* fall through */
204         case '*':
205             buffer[i] = '?';
206             break;
207         default:
208             if (strchr( invalid_chars, *p )) return NULL;
209             buffer[i] = toupper(*p);
210             p++;
211             break;
212         }
213     }
214     buffer[11] = '\0';
215     return buffer;
216 }
217
218
219 /***********************************************************************
220  *           DOSFS_ToDosDTAFormat
221  *
222  * Convert a file name from FCB to DTA format (name.ext, null-terminated)
223  * converting to upper-case in the process.
224  * File name can be terminated by '\0', '\\' or '/'.
225  * Return NULL if the name is not a valid DOS name.
226  */
227 const char *DOSFS_ToDosDTAFormat( const char *name )
228 {
229     static char buffer[13];
230     char *p;
231
232     memcpy( buffer, name, 8 );
233     for (p = buffer + 8; (p > buffer) && (p[-1] == ' '); p--);
234     *p++ = '.';
235     memcpy( p, name + 8, 3 );
236     for (p += 3; p[-1] == ' '; p--);
237     if (p[-1] == '.') p--;
238     *p = '\0';
239     return buffer;
240 }
241
242
243 /***********************************************************************
244  *           DOSFS_Match
245  *
246  * Check a DOS file name against a mask (both in FCB format).
247  */
248 static int DOSFS_Match( const char *mask, const char *name )
249 {
250     int i;
251     for (i = 11; i > 0; i--, mask++, name++)
252         if ((*mask != '?') && (*mask != *name)) return 0;
253     return 1;
254 }
255
256
257 /***********************************************************************
258  *           DOSFS_ToDosDateTime
259  *
260  * Convert a Unix time in the DOS date/time format.
261  */
262 void DOSFS_ToDosDateTime( time_t unixtime, WORD *pDate, WORD *pTime )
263 {
264     struct tm *tm = localtime( &unixtime );
265     if (pTime)
266         *pTime = (tm->tm_hour << 11) + (tm->tm_min << 5) + (tm->tm_sec / 2);
267     if (pDate)
268         *pDate = ((tm->tm_year - 80) << 9) + ((tm->tm_mon + 1) << 5)
269                  + tm->tm_mday;
270 }
271
272
273 /***********************************************************************
274  *           DOSFS_Hash
275  *
276  * Transform a Unix file name into a hashed DOS name. If the name is a valid
277  * DOS name, it is converted to upper-case; otherwise it is replaced by a
278  * hashed version that fits in 8.3 format.
279  * File name can be terminated by '\0', '\\' or '/'.
280  */
281 static const char *DOSFS_Hash( const char *name, int dir_format )
282 {
283     static const char invalid_chars[] = INVALID_DOS_CHARS "~.";
284     static const char hash_chars[32] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
285
286     static char buffer[13];
287     const char *p, *ext;
288     char *dst;
289     unsigned short hash;
290     int i;
291
292     if (dir_format) strcpy( buffer, "           " );
293
294     if (DOSFS_ValidDOSName( name ))
295     {
296         /* Check for '.' and '..' */
297         if (*name == '.')
298         {
299             buffer[0] = '.';
300             if (!dir_format) buffer[1] = buffer[2] = '\0';
301             if (name[1] == '.') buffer[1] = '.';
302             return buffer;
303         }
304
305         /* Simply copy the name, converting to uppercase */
306
307         for (dst = buffer; !IS_END_OF_NAME(*name) && (*name != '.'); name++)
308             *dst++ = toupper(*name);
309         if (*name == '.')
310         {
311             if (dir_format) dst = buffer + 8;
312             else *dst++ = '.';
313             for (name++; !IS_END_OF_NAME(*name); name++)
314                 *dst++ = toupper(*name);
315         }
316         if (!dir_format) *dst = '\0';
317     }
318     else
319     {
320         /* Compute the hash code of the file name */
321         /* If you know something about hash functions, feel free to */
322         /* insert a better algorithm here... */
323         for (p = name, hash = 0xbeef; !IS_END_OF_NAME(p[1]); p++)
324             hash = (hash << 3) ^ (hash >> 5) ^ *p ^ (p[1] << 8);
325         hash = (hash << 3) ^ (hash >> 5) ^ *p;  /* Last character */
326         
327         /* Find last dot for start of the extension */
328         for (p = name+1, ext = NULL; !IS_END_OF_NAME(*p); p++)
329             if (*p == '.') ext = p;
330         if (ext && IS_END_OF_NAME(ext[1]))
331             ext = NULL;  /* Empty extension ignored */
332
333         /* Copy first 4 chars, replacing invalid chars with '_' */
334         for (i = 4, p = name, dst = buffer; i > 0; i--, p++)
335         {
336             if (IS_END_OF_NAME(*p) || (p == ext)) break;
337             *dst++ = strchr( invalid_chars, *p ) ? '_' : toupper(*p);
338         }
339         /* Pad to 5 chars with '~' */
340         while (i-- >= 0) *dst++ = '~';
341
342         /* Insert hash code converted to 3 ASCII chars */
343         *dst++ = hash_chars[(hash >> 10) & 0x1f];
344         *dst++ = hash_chars[(hash >> 5) & 0x1f];
345         *dst++ = hash_chars[hash & 0x1f];
346
347         /* Copy the first 3 chars of the extension (if any) */
348         if (ext)
349         {
350             if (!dir_format) *dst++ = '.';
351             for (i = 3, ext++; (i > 0) && !IS_END_OF_NAME(*ext); i--, ext++)
352                 *dst++ = toupper(*ext);
353         }
354         if (!dir_format) *dst = '\0';
355     }
356     return buffer;
357 }
358
359
360 /***********************************************************************
361  *           DOSFS_FindUnixName
362  *
363  * Find the Unix file name in a given directory that corresponds to
364  * a file name (either in Unix or DOS format).
365  * File name can be terminated by '\0', '\\' or '/'.
366  * Return 1 if OK, 0 if no file name matches.
367  */
368 static int DOSFS_FindUnixName( const char *path, const char *name,
369                                char *buffer, int maxlen )
370 {
371     DIR *dir;
372     struct dirent *dirent;
373
374     const char *dos_name = DOSFS_ToDosFCBFormat( name );
375     const char *p = strchr( name, '/' );
376     int len = p ? (int)(p - name) : strlen(name);
377
378     dprintf_dosfs( stddeb, "DOSFS_FindUnixName: %s %s\n", path, name );
379
380     if ((p = strchr( name, '\\' ))) len = MIN( (int)(p - name), len );
381
382     if (!(dir = opendir( path )))
383     {
384         dprintf_dosfs( stddeb, "DOSFS_FindUnixName(%s,%s): can't open dir\n",
385                        path, name );
386         return 0;
387     }
388     while ((dirent = readdir( dir )) != NULL)
389     {
390         /* Check against Unix name */
391         if ((len == strlen(dirent->d_name) &&
392              !memcmp( dirent->d_name, name, len ))) break;
393         if (dos_name)
394         {
395             /* Check against hashed DOS name */
396             const char *hash_name = DOSFS_Hash( dirent->d_name, TRUE );
397             if (!strcmp( dos_name, hash_name )) break;
398         }
399     }
400     if (dirent) lstrcpyn( buffer, dirent->d_name, maxlen );
401     closedir( dir );
402     dprintf_dosfs( stddeb, "DOSFS_FindUnixName(%s,%s) -> %s\n",
403                    path, name, dirent ? buffer : "** Not found **" );
404     return (dirent != NULL);
405 }
406
407
408 /***********************************************************************
409  *           DOSFS_IsDevice
410  *
411  * Check if a DOS file name represents a DOS device. Returns the name
412  * of the associated Unix device, or NULL if not found.
413  */
414 const char *DOSFS_IsDevice( const char *name )
415 {
416     int i;
417     const char *p;
418
419     if (name[0] && (name[1] == ':')) name += 2;
420     if ((p = strrchr( name, '/' ))) name = p + 1;
421     if ((p = strrchr( name, '\\' ))) name = p + 1;
422     for (i = 0; i < sizeof(DOSFS_Devices)/sizeof(DOSFS_Devices[0]); i++)
423     {
424         const char *dev = DOSFS_Devices[i][0];
425         if (!lstrncmpi( dev, name, strlen(dev) ))
426         {
427             p = name + strlen( dev );
428             if (!*p || (*p == '.')) return DOSFS_Devices[i][1];
429         }
430     }
431     return NULL;
432 }
433
434
435 /***********************************************************************
436  *           DOSFS_GetUnixFileName
437  *
438  * Convert a file name (DOS or mixed DOS/Unix format) to a valid Unix name.
439  * Return NULL if one of the path components does not exist. The last path
440  * component is only checked if 'check_last' is non-zero.
441  */
442 const char * DOSFS_GetUnixFileName( const char * name, int check_last )
443 {
444     static char buffer[MAX_PATHNAME_LEN];
445     int drive, len, found;
446     char *p, *root;
447
448     dprintf_dosfs( stddeb, "DOSFS_GetUnixFileName: %s\n", name );
449     if (name[0] && (name[1] == ':'))
450     {
451         drive = toupper(name[0]) - 'A';
452         name += 2;
453     }
454     else if (name[0] == '/') /* Absolute Unix path? */
455     {
456         if ((drive = DRIVE_FindDriveRoot( &name )) == -1)
457         {
458             fprintf( stderr, "Warning: %s not accessible from a DOS drive\n",
459                      name );
460             /* Assume it really was a DOS name */
461             drive = DRIVE_GetCurrentDrive();            
462         }
463     }
464     else drive = DRIVE_GetCurrentDrive();
465
466     if (!DRIVE_IsValid(drive))
467     {
468         DOS_ERROR( ER_InvalidDrive, EC_MediaError, SA_Abort, EL_Disk );
469         return NULL;
470     }
471     lstrcpyn( buffer, DRIVE_GetRoot(drive), MAX_PATHNAME_LEN );
472     if (buffer[1]) root = buffer + strlen(buffer);
473     else root = buffer;  /* root directory */
474
475     if ((*name == '\\') || (*name == '/'))
476     {
477         while ((*name == '\\') || (*name == '/')) name++;
478     }
479     else
480     {
481         lstrcpyn( root + 1, DRIVE_GetUnixCwd(drive),
482                   MAX_PATHNAME_LEN - (int)(root - buffer) - 1 );
483         if (root[1]) *root = '/';
484     }
485
486     p = buffer[1] ? buffer + strlen(buffer) : buffer;
487     len = MAX_PATHNAME_LEN - strlen(buffer);
488     found = 1;
489     while (*name && found)
490     {
491         const char *newname = DOSFS_CheckDotDot( name, root, '/', &len );
492         if (newname != name)
493         {
494             p = root + strlen(root);
495             name = newname;
496             continue;
497         }
498         if (len <= 1)
499         {
500             DOS_ERROR( ER_PathNotFound, EC_NotFound, SA_Abort, EL_Disk );
501             return NULL;
502         }
503         if ((found = DOSFS_FindUnixName( buffer, name, p+1, len-1 )))
504         {
505             *p = '/';
506             len -= strlen(p);
507             p += strlen(p);
508             while (!IS_END_OF_NAME(*name)) name++;
509         }
510         else if (!check_last)
511         {
512             *p++ = '/';
513             for (len--; !IS_END_OF_NAME(*name) && (len > 1); name++, len--)
514                 *p++ = tolower(*name);
515             *p = '\0';
516         }
517         while ((*name == '\\') || (*name == '/')) name++;
518     }
519     if (!found)
520     {
521         if (check_last)
522         {
523             DOS_ERROR( ER_FileNotFound, EC_NotFound, SA_Abort, EL_Disk );
524             return NULL;
525         }
526         if (*name)  /* Not last */
527         {
528             DOS_ERROR( ER_PathNotFound, EC_NotFound, SA_Abort, EL_Disk );
529             return NULL;
530         }
531     }
532     if (!buffer[0]) strcpy( buffer, "/" );
533     dprintf_dosfs( stddeb, "DOSFS_GetUnixFileName: returning %s\n", buffer );
534     return buffer;
535 }
536
537
538 /***********************************************************************
539  *           DOSFS_GetDosTrueName
540  *
541  * Convert a file name (DOS or Unix format) to a complete DOS name.
542  * Return NULL if the path name is invalid or too long.
543  * The unix_format flag is a hint that the file name is in Unix format.
544  */
545 const char * DOSFS_GetDosTrueName( const char *name, int unix_format )
546 {
547     static char buffer[MAX_PATHNAME_LEN];
548     int drive, len;
549     char *p;
550
551     dprintf_dosfs( stddeb, "DOSFS_GetDosTrueName(%s,%d)\n", name, unix_format);
552     if (name[0] && (name[1] == ':'))
553     {
554         drive = toupper(name[0]) - 'A';
555         name += 2;
556     }
557     else if (name[0] == '/') /* Absolute Unix path? */
558     {
559         if ((drive = DRIVE_FindDriveRoot( &name )) == -1)
560         {
561             fprintf( stderr, "Warning: %s not accessible from a DOS drive\n",
562                      name );
563             /* Assume it really was a DOS name */
564             drive = DRIVE_GetCurrentDrive();            
565         }
566     }
567     else drive = DRIVE_GetCurrentDrive();
568
569     if (!DRIVE_IsValid(drive))
570     {
571         DOS_ERROR( ER_InvalidDrive, EC_MediaError, SA_Abort, EL_Disk );
572         return NULL;
573     }
574
575     p = buffer;
576     *p++ = 'A' + drive;
577     *p++ = ':';
578     if (IS_END_OF_NAME(*name))
579     {
580         while ((*name == '\\') || (*name == '/')) name++;
581     }
582     else
583     {
584         *p++ = '\\';
585         lstrcpyn( p, DRIVE_GetDosCwd(drive), sizeof(buffer) - 3 );
586         if (*p) p += strlen(p); else p--;
587     }
588     *p = '\0';
589     len = MAX_PATHNAME_LEN - (int)(p - buffer);
590
591     while (*name)
592     {
593         const char *newname = DOSFS_CheckDotDot( name, buffer+2, '\\', &len );
594         if (newname != name)
595         {
596             p = buffer + strlen(buffer);
597             name = newname;
598             continue;
599         }
600         if (len <= 1)
601         {
602             DOS_ERROR( ER_PathNotFound, EC_NotFound, SA_Abort, EL_Disk );
603             return NULL;
604         }
605         *p++ = '\\';
606         if (unix_format)  /* Hash it into a DOS name */
607         {
608             lstrcpyn( p, DOSFS_Hash( name, FALSE ), len );
609             len -= strlen(p);
610             p += strlen(p);
611             while (!IS_END_OF_NAME(*name)) name++;
612         }
613         else  /* Already DOS format, simply upper-case it */
614         {
615             while (!IS_END_OF_NAME(*name) && (len > 1))
616             {
617                 *p++ = toupper(*name);
618                 name++;
619                 len--;
620             }
621             *p = '\0';
622         }
623         while ((*name == '\\') || (*name == '/')) name++;
624     }
625     if (!buffer[2])
626     {
627         buffer[2] = '\\';
628         buffer[3] = '\0';
629     }
630     dprintf_dosfs( stddeb, "DOSFS_GetDosTrueName: returning %s\n", buffer );
631     return buffer;
632 }
633
634
635 /***********************************************************************
636  *           DOSFS_FindNext
637  *
638  * Find the next matching file. Return the number of entries read to find
639  * the matching one, or 0 if no more entries.
640  */
641 int DOSFS_FindNext( const char *path, const char *mask, int drive,
642                     BYTE attr, int skip, DOS_DIRENT *entry )
643 {
644     static DIR *dir = NULL;
645     struct dirent *dirent;
646     int count = 0;
647     static char buffer[MAX_PATHNAME_LEN];
648     static int cur_pos = 0;
649     static int drive_root = 0;
650     char *p;
651     const char *hash_name;
652     
653     if ((attr & ~(FA_UNUSED | FA_ARCHIVE | FA_RDONLY)) == FA_LABEL)
654     {
655         if (skip) return 0;
656         strcpy( entry->name, DRIVE_GetLabel( drive ) );
657         entry->attr = FA_LABEL;
658         entry->size = 0;
659         DOSFS_ToDosDateTime( time(NULL), &entry->date, &entry->time );
660         return 1;
661     }
662
663     /* Check the cached directory */
664     if (dir && !strcmp( buffer, path ) && (cur_pos <= skip)) skip -= cur_pos;
665     else  /* Not in the cache, open it anew */
666     {
667         const char *drive_path;
668         dprintf_dosfs( stddeb, "DOSFS_FindNext: cache miss, path=%s skip=%d buf=%s cur=%d\n",
669                        path, skip, buffer, cur_pos );
670         cur_pos = skip;
671         if (dir) closedir(dir);
672         if (!(dir = opendir( path ))) return 0;
673         drive_path = path;
674         drive_root = 0;
675         if (DRIVE_FindDriveRoot( &drive_path ) != -1)
676         {
677             while ((*drive_path == '/') || (*drive_path == '\\')) drive_path++;
678             if (!*drive_path) drive_root = 1;
679         }
680         dprintf_dosfs(stddeb, "DOSFS_FindNext: drive_root = %d\n", drive_root);
681         lstrcpyn( buffer, path, sizeof(buffer) - 1 );
682         
683     }
684     strcat( buffer, "/" );
685     p = buffer + strlen(buffer);
686     attr |= FA_UNUSED | FA_ARCHIVE | FA_RDONLY;
687
688     while ((dirent = readdir( dir )) != NULL)
689     {
690         if (skip-- > 0) continue;
691         count++;
692         hash_name = DOSFS_Hash( dirent->d_name, TRUE );
693         if (!DOSFS_Match( mask, hash_name )) continue;
694         /* Don't return '.' and '..' in the root of the drive */
695         if (drive_root && (dirent->d_name[0] == '.') &&
696             (!dirent->d_name[1] ||
697              ((dirent->d_name[1] == '.') && !dirent->d_name[2]))) continue;
698         lstrcpyn( p, dirent->d_name, sizeof(buffer) - (int)(p - buffer) );
699
700         if (!FILE_Stat( buffer, &entry->attr, &entry->size,
701                         &entry->date, &entry->time ))
702         {
703             fprintf( stderr, "DOSFS_FindNext: can't stat %s\n", buffer );
704             continue;
705         }
706         if (entry->attr & ~attr) continue;
707         strcpy( entry->name, hash_name );
708         lstrcpyn( entry->unixname, dirent->d_name, sizeof(entry->unixname) );
709         dprintf_dosfs( stddeb, "DOSFS_FindNext: returning %s %02x %ld\n",
710                        entry->name, entry->attr, entry->size );
711         cur_pos += count;
712         p[-1] = '\0';  /* Remove trailing slash in buffer */
713         return count;
714     }
715     closedir( dir );
716     dir = NULL;
717     return 0;  /* End of directory */
718 }