#include "precompiled.h" #include "wfilesystem.h" #include "lib/allocators.h" // single_calloc #include "wposix_internal.h" #include "wtime_internal.h" // wtime_utc_filetime_to_time_t #include "crt_posix.h" // _rmdir, _access // // determine file system type on the current drive - // needed to work around incorrect FAT time translation. // static enum Filesystem { FS_INVALID, // detect_filesystem() not yet called FS_FAT, // FAT12, FAT16, or FAT32 FS_NTFS, // (most common) FS_UNKNOWN // newer FS we don't know about } filesystem; // rationale: the previous method of checking every path was way too slow // (taking ~800ms total during init). instead, we only determine the FS once. // this is quite a bit easier than intercepting chdir() calls and/or // caching FS type per drive letter, but not foolproof. // // if some data files are on a different volume that is set up as FAT, // the workaround below won't be triggered (=> timestamps may be off by // 1 hour when DST is in effect). oh well, that is not a supported. // // the common case (everything is on a single NTFS volume) is more important // and must run without penalty. // called from the first filetime_to_time_t() call, not win.cpp init; // this means we can rely on the current directory having been set to // the app's directory (and therefore its appendant volume - see above). static void detect_filesystem() { char root_path[MAX_PATH] = "c:\\"; // default in case GCD fails DWORD gcd_ret = GetCurrentDirectory(sizeof(root_path), root_path); debug_assert(gcd_ret != 0); // if this fails, no problem - we have the default from above. root_path[3] = '\0'; // cut off after "c:\" char fs_name[32] = {0}; BOOL ret = GetVolumeInformation(root_path, 0,0,0,0,0, fs_name, sizeof(fs_name)); fs_name[ARRAY_SIZE(fs_name)-1] = '\0'; debug_assert(ret != 0); // if this fails, no problem - we really only care if fs is FAT, // and will assume that's not the case (since fs_name != "FAT"). filesystem = FS_UNKNOWN; if(!strncmp(fs_name, "FAT", 3)) // e.g. FAT32 filesystem = FS_FAT; else if(!strcmp(fs_name, "NTFS")) filesystem = FS_NTFS; } // convert local FILETIME (includes timezone bias and possibly DST bias) // to seconds-since-1970 UTC. // // note: splitting into month, year etc. is inefficient, // but much easier than determining whether ft lies in DST, // and ourselves adding the appropriate bias. // // called for FAT file times; see wposix filetime_to_time_t. time_t time_t_from_local_filetime(FILETIME* ft) { SYSTEMTIME st; FileTimeToSystemTime(ft, &st); struct tm t; t.tm_sec = st.wSecond; t.tm_min = st.wMinute; t.tm_hour = st.wHour; t.tm_mday = st.wDay; t.tm_mon = st.wMonth-1; t.tm_year = st.wYear-1900; t.tm_isdst = -1; // let the CRT determine whether this local time // falls under DST by the US rules. return mktime(&t); } // convert Windows FILETIME to POSIX time_t (seconds-since-1970 UTC); // used by stat and readdir_stat_np for st_mtime. // // works around a documented Windows bug in converting FAT file times // (correct results are desired since VFS mount logic considers // files 'equal' if their mtime and size are the same). static time_t filetime_to_time_t(FILETIME* ft) { ONCE(detect_filesystem()); // the FAT file system stores local file times, while // NTFS records UTC. Windows does convert automatically, // but uses the current DST settings. (boo!) // we go back to local time, and convert properly. if(filesystem == FS_FAT) { FILETIME local_ft; FileTimeToLocalFileTime(ft, &local_ft); return time_t_from_local_filetime(&local_ft); } return wtime_utc_filetime_to_time_t(ft); } /* // currently only sets st_mode (file or dir) and st_size. int stat(const char* fn, struct stat* s) { memset(s, 0, sizeof(struct stat)); WIN32_FILE_ATTRIBUTE_DATA fad; if(!GetFileAttributesEx(fn, GetFileExInfoStandard, &fad)) return -1; s->st_mtime = filetime_to_time_t(fad.ftLastAccessTime) // dir if(fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) s->st_mode = S_IFDIR; else { s->st_mode = S_IFREG; s->st_size = (off_t)((((u64)fad.nFileSizeHigh) << 32) | fad.nFileSizeLow); } return 0; } */ int access(const char* path, int mode) { return _access(path, mode); } #if !HAVE_MKDIR int mkdir(const char* path, mode_t UNUSED(mode)) { if(!CreateDirectory(path, (LPSECURITY_ATTRIBUTES)NULL)) { return -1; } return 0; } #endif // #if !HAVE_MKDIR int rmdir(const char* path) { return _rmdir(path); } //----------------------------------------------------------------------------- // readdir //----------------------------------------------------------------------------- // note: we avoid opening directories or returning entries that have // hidden or system attributes set. this is to prevent returning something // like "\System Volume Information", which raises an error upon opening. // 0-initialized by wdir_alloc for safety; this is required for // num_entries_scanned. struct WDIR { HANDLE hFind; // the dirent returned by readdir. // note: having only one global instance is not possible because // multiple independent opendir/readdir sequences must be supported. struct dirent ent; WIN32_FIND_DATA fd; // since opendir calls FindFirstFile, we need a means of telling the // first call to readdir that we already have a file. // that's the case iff this is == 0; we use a counter rather than a // flag because that allows keeping statistics. int num_entries_scanned; }; // suballocator - satisfies most requests with a reusable static instance, // thus speeding up allocation and avoiding heap fragmentation. // thread-safe. static WDIR global_wdir; static uintptr_t global_wdir_is_in_use; // zero-initializes the WDIR (code below relies on this) static inline WDIR* wdir_alloc() { return (WDIR*)single_calloc(&global_wdir, &global_wdir_is_in_use, sizeof(WDIR)); } static inline void wdir_free(WDIR* d) { single_free(&global_wdir, &global_wdir_is_in_use, d); } static const DWORD hs = FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM; // make sure path exists and is a normal (according to attributes) directory. static bool is_normal_dir(const char* path) { const DWORD fa = GetFileAttributes(path); // path not found if(fa == INVALID_FILE_ATTRIBUTES) return false; // not a directory if((fa & FILE_ATTRIBUTE_DIRECTORY) == 0) return false; // hidden or system attribute(s) set // this check is now disabled because wsnd's add_oal_dlls_in_dir // needs to open the Windows system directory, which sometimes has // these attributes set. //if((fa & hs) != 0) // return false; return true; } DIR* opendir(const char* path) { if(!is_normal_dir(path)) { errno = ENOENT; fail: debug_warn("opendir failed"); return 0; } WDIR* d = wdir_alloc(); if(!d) { errno = ENOMEM; goto fail; } // build search path for FindFirstFile. note: "path\\dir" only returns // information about that directory; trailing slashes aren't allowed. // for dir entries to be returned, we have to append "\\*". char search_path[PATH_MAX]; snprintf(search_path, ARRAY_SIZE(search_path), "%s\\*", path); // note: we could store search_path and defer FindFirstFile until // readdir. this way is a bit more complex but required for // correctness (we must return a valid DIR iff is valid). d->hFind = FindFirstFileA(search_path, &d->fd); if(d->hFind == INVALID_HANDLE_VALUE) { // not an error - the directory is just empty. if(GetLastError() == ERROR_NO_MORE_FILES) goto success; // translate Win32 error to errno. LibError err = LibError_from_win32(FALSE); LibError_set_errno(err); // release the WDIR allocated above. // unfortunately there's no way around this; we need to allocate // d before FindFirstFile because it uses d->fd. copying from a // temporary isn't nice either (this free doesn't happen often) wdir_free(d); goto fail; } success: return d; } struct dirent* readdir(DIR* d_) { WDIR* const d = (WDIR*)d_; // avoid polluting the last error. DWORD prev_err = GetLastError(); // first call - skip FindNextFile (see opendir). if(d->num_entries_scanned == 0) { // this directory is empty. if(d->hFind == INVALID_HANDLE_VALUE) return 0; goto already_have_file; } // until end of directory or a valid entry was found: for(;;) { if(!FindNextFileA(d->hFind, &d->fd)) goto fail; already_have_file: d->num_entries_scanned++; // not a hidden or system entry -> it's valid. if((d->fd.dwFileAttributes & hs) == 0) break; } // this entry has passed all checks; return information about it. // (note: d_name is a pointer; see struct dirent definition) d->ent.d_name = d->fd.cFileName; return &d->ent; fail: // FindNextFile failed; determine why and bail. // .. legit, end of dir reached. don't pollute last error code. if(GetLastError() == ERROR_NO_MORE_FILES) SetLastError(prev_err); else debug_warn("readdir: FindNextFile failed"); return 0; } // return status for the dirent returned by the last successful // readdir call from the given directory stream. // currently sets st_size, st_mode, and st_mtime; the rest are zeroed. // non-portable, but considerably faster than stat(). used by file_enum. int readdir_stat_np(DIR* d_, struct stat* s) { WDIR* d = (WDIR*)d_; memset(s, 0, sizeof(*s)); s->st_size = (off_t)u64_from_u32(d->fd.nFileSizeHigh, d->fd.nFileSizeLow); s->st_mode = (d->fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)? S_IFDIR : S_IFREG; s->st_mtime = filetime_to_time_t(&d->fd.ftLastWriteTime); return 0; } int closedir(DIR* d_) { WDIR* const d = (WDIR*)d_; FindClose(d->hFind); wdir_free(d); return 0; } //----------------------------------------------------------------------------- char* realpath(const char* fn, char* path) { if(!GetFullPathName(fn, PATH_MAX, path, 0)) return 0; return path; }