#include "../../cache.h" #include "../../hashmap.h" #include "../win32.h" #include "fscache.h" static int initialized; static volatile long enabled; static struct hashmap map; static CRITICAL_SECTION mutex; /* * An entry in the file system cache. Used for both entire directory listings * and file entries. */ struct fsentry { struct hashmap_entry ent; mode_t st_mode; /* Length of name. */ unsigned short len; /* * Name of the entry. For directory listings: relative path of the * directory, without trailing '/' (empty for cwd()). For file entries: * name of the file. Typically points to the end of the structure if * the fsentry is allocated on the heap (see fsentry_alloc), or to a * local variable if on the stack (see fsentry_init). */ const char *name; /* Pointer to the directory listing, or NULL for the listing itself. */ struct fsentry *list; /* Pointer to the next file entry of the list. */ struct fsentry *next; union { /* Reference count of the directory listing. */ volatile long refcnt; /* Handle to wait on the loading thread. */ HANDLE hwait; struct { /* More stat members (only used for file entries). */ off64_t st_size; time_t st_atime; time_t st_mtime; time_t st_ctime; }; }; }; /* * Compares the paths of two fsentry structures for equality. */ static int fsentry_cmp(const struct fsentry *fse1, const struct fsentry *fse2) { int res; if (fse1 == fse2) return 0; /* compare the list parts first */ if (fse1->list != fse2->list && (res = fsentry_cmp( fse1->list ? fse1->list : fse1, fse2->list ? fse2->list : fse2))) return res; /* if list parts are equal, compare len and name */ if (fse1->len != fse2->len) return fse1->len - fse2->len; return strnicmp(fse1->name, fse2->name, fse1->len); } /* * Calculates the hash code of an fsentry structure's path. */ static unsigned int fsentry_hash(const struct fsentry *fse) { unsigned int hash = fse->list ? fse->list->ent.hash : 0; return hash ^ memihash(fse->name, fse->len); } /* * Initialize an fsentry structure for use by fsentry_hash and fsentry_cmp. */ static void fsentry_init(struct fsentry *fse, struct fsentry *list, const char *name, size_t len) { fse->list = list; fse->name = name; fse->len = len; hashmap_entry_init(fse, fsentry_hash(fse)); } /* * Allocate an fsentry structure on the heap. */ static struct fsentry *fsentry_alloc(struct fsentry *list, const char *name, size_t len) { /* overallocate fsentry and copy the name to the end */ struct fsentry *fse = xmalloc(sizeof(struct fsentry) + len + 1); char *nm = ((char*) fse) + sizeof(struct fsentry); memcpy(nm, name, len); nm[len] = 0; /* init the rest of the structure */ fsentry_init(fse, list, nm, len); fse->next = NULL; fse->refcnt = 1; return fse; } /* * Add a reference to an fsentry. */ inline static void fsentry_addref(struct fsentry *fse) { if (fse->list) fse = fse->list; InterlockedIncrement(&(fse->refcnt)); } /* * Release the reference to an fsentry, frees the memory if its the last ref. */ static void fsentry_release(struct fsentry *fse) { if (fse->list) fse = fse->list; if (InterlockedDecrement(&(fse->refcnt))) return; while (fse) { struct fsentry *next = fse->next; free(fse); fse = next; } } /* * Allocate and initialize an fsentry from a WIN32_FIND_DATA structure. */ static struct fsentry *fseentry_create_entry(struct fsentry *list, const WIN32_FIND_DATAW *fdata) { char buf[MAX_PATH * 3]; int len; struct fsentry *fse; len = xwcstoutf(buf, fdata->cFileName, ARRAY_SIZE(buf)); fse = fsentry_alloc(list, buf, len); fse->st_mode = file_attr_to_st_mode(fdata->dwFileAttributes); fse->st_size = (((off64_t) (fdata->nFileSizeHigh)) << 32) | fdata->nFileSizeLow; fse->st_atime = filetime_to_time_t(&(fdata->ftLastAccessTime)); fse->st_mtime = filetime_to_time_t(&(fdata->ftLastWriteTime)); fse->st_ctime = filetime_to_time_t(&(fdata->ftCreationTime)); return fse; } /* * Create an fsentry-based directory listing (similar to opendir / readdir). * Dir should not contain trailing '/'. Use an empty string for the current * directory (not "."!). */ static struct fsentry *fsentry_create_list(const struct fsentry *dir) { wchar_t pattern[MAX_LONG_PATH + 2]; /* + 2 for "\*" */ WIN32_FIND_DATAW fdata; HANDLE h; int wlen; struct fsentry *list, **phead; DWORD err; /* convert name to UTF-16 and check length */ if ((wlen = xutftowcs_path_ex(pattern, dir->name, MAX_LONG_PATH, dir->len, MAX_PATH - 2, core_long_paths)) < 0) return NULL; /* * append optional '\' and wildcard '*'. Note: we need to use '\' as * Windows doesn't translate '/' to '\' for "\\?\"-prefixed paths. */ if (wlen) pattern[wlen++] = '\\'; pattern[wlen++] = '*'; pattern[wlen] = 0; /* open find handle */ h = FindFirstFileW(pattern, &fdata); if (h == INVALID_HANDLE_VALUE) { err = GetLastError(); errno = (err == ERROR_DIRECTORY) ? ENOTDIR : err_win_to_posix(err); return NULL; } /* allocate object to hold directory listing */ list = fsentry_alloc(NULL, dir->name, dir->len); /* walk directory and build linked list of fsentry structures */ phead = &list->next; do { *phead = fseentry_create_entry(list, &fdata); phead = &(*phead)->next; } while (FindNextFileW(h, &fdata)); /* remember result of last FindNextFile, then close find handle */ err = GetLastError(); FindClose(h); /* return the list if we've got all the files */ if (err == ERROR_NO_MORE_FILES) return list; /* otherwise free the list and return error */ fsentry_release(list); errno = err_win_to_posix(err); return NULL; } /* * Adds a directory listing to the cache. */ static void fscache_add(struct fsentry *fse) { if (fse->list) fse = fse->list; for (; fse; fse = fse->next) hashmap_add(&map, fse); } /* * Removes a directory listing from the cache. */ static void fscache_remove(struct fsentry *fse) { if (fse->list) fse = fse->list; for (; fse; fse = fse->next) hashmap_remove(&map, fse, NULL); } /* * Clears the cache. */ static void fscache_clear() { struct hashmap_iter iter; struct fsentry *fse; while ((fse = hashmap_iter_first(&map, &iter))) { fscache_remove(fse); fsentry_release(fse); } } /* * Checks if the cache is enabled for the given path. */ static inline int fscache_enabled(const char *path) { return enabled > 0 && !is_absolute_path(path); } /* * Looks up a cache entry, waits if its being loaded by another thread. * The mutex must be owned by the calling thread. */ static struct fsentry *fscache_get_wait(struct fsentry *key) { struct fsentry *fse = hashmap_get(&map, key, NULL); /* return if its a 'real' entry (future entries have refcnt == 0) */ if (!fse || fse->list || fse->refcnt) return fse; /* create an event and link our key to the future entry */ key->hwait = CreateEvent(NULL, TRUE, FALSE, NULL); key->next = fse->next; fse->next = key; /* wait for the loading thread to signal us */ LeaveCriticalSection(&mutex); WaitForSingleObject(key->hwait, INFINITE); CloseHandle(key->hwait); EnterCriticalSection(&mutex); /* repeat cache lookup */ return hashmap_get(&map, key, NULL); } /* * Looks up or creates a cache entry for the specified key. */ static struct fsentry *fscache_get(struct fsentry *key) { struct fsentry *fse, *future, *waiter; EnterCriticalSection(&mutex); /* check if entry is in cache */ fse = fscache_get_wait(key); if (fse) { fsentry_addref(fse); LeaveCriticalSection(&mutex); return fse; } /* if looking for a file, check if directory listing is in cache */ if (!fse && key->list) { fse = fscache_get_wait(key->list); if (fse) { LeaveCriticalSection(&mutex); /* dir entry without file entry -> file doesn't exist */ errno = ENOENT; return NULL; } } /* add future entry to indicate that we're loading it */ future = key->list ? key->list : key; future->next = NULL; future->refcnt = 0; hashmap_add(&map, future); /* create the directory listing (outside mutex!) */ LeaveCriticalSection(&mutex); fse = fsentry_create_list(future); EnterCriticalSection(&mutex); /* remove future entry and signal waiting threads */ hashmap_remove(&map, future, NULL); waiter = future->next; while (waiter) { HANDLE h = waiter->hwait; waiter = waiter->next; SetEvent(h); } /* leave on error (errno set by fsentry_create_list) */ if (!fse) { LeaveCriticalSection(&mutex); return NULL; } /* add directory listing to the cache */ fscache_add(fse); /* lookup file entry if requested (fse already points to directory) */ if (key->list) fse = hashmap_get(&map, key, NULL); /* return entry or ENOENT */ if (fse) fsentry_addref(fse); else errno = ENOENT; LeaveCriticalSection(&mutex); return fse; } /* * Enables or disables the cache. Note that the cache is read-only, changes to * the working directory are NOT reflected in the cache while enabled. */ int fscache_enable(int enable) { int result; if (!initialized) { /* allow the cache to be disabled entirely */ if (!core_fscache) return 0; InitializeCriticalSection(&mutex); hashmap_init(&map, (hashmap_cmp_fn) fsentry_cmp, 0); initialized = 1; } result = enable ? InterlockedIncrement(&enabled) : InterlockedDecrement(&enabled); if (enable && result == 1) { /* redirect opendir and lstat to the fscache implementations */ opendir = fscache_opendir; lstat = fscache_lstat; } else if (!enable && !result) { /* reset opendir and lstat to the original implementations */ opendir = dirent_opendir; lstat = mingw_lstat; EnterCriticalSection(&mutex); fscache_clear(); LeaveCriticalSection(&mutex); } return result; } /* * Lstat replacement, uses the cache if enabled, otherwise redirects to * mingw_lstat. */ int fscache_lstat(const char *filename, struct stat *st) { int dirlen, base, len; struct fsentry key[2], *fse; if (!fscache_enabled(filename)) return mingw_lstat(filename, st); /* split filename into path + name */ len = strlen(filename); if (len && is_dir_sep(filename[len - 1])) len--; base = len; while (base && !is_dir_sep(filename[base - 1])) base--; dirlen = base ? base - 1 : 0; /* lookup entry for path + name in cache */ fsentry_init(key, NULL, filename, dirlen); fsentry_init(key + 1, key, filename + base, len - base); fse = fscache_get(key + 1); if (!fse) return -1; /* copy stat data */ st->st_ino = 0; st->st_gid = 0; st->st_uid = 0; st->st_dev = 0; st->st_rdev = 0; st->st_nlink = 1; st->st_mode = fse->st_mode; st->st_size = fse->st_size; st->st_atime = fse->st_atime; st->st_mtime = fse->st_mtime; st->st_ctime = fse->st_ctime; /* don't forget to release fsentry */ fsentry_release(fse); return 0; } typedef struct fscache_DIR { struct DIR base_dir; /* extend base struct DIR */ struct fsentry *pfsentry; struct dirent dirent; } fscache_DIR; /* * Readdir replacement. */ static struct dirent *fscache_readdir(DIR *base_dir) { fscache_DIR *dir = (fscache_DIR*) base_dir; struct fsentry *next = dir->pfsentry->next; if (!next) return NULL; dir->pfsentry = next; dir->dirent.d_type = S_ISDIR(next->st_mode) ? DT_DIR : DT_REG; dir->dirent.d_name = (char*) next->name; return &(dir->dirent); } /* * Closedir replacement. */ static int fscache_closedir(DIR *base_dir) { fscache_DIR *dir = (fscache_DIR*) base_dir; fsentry_release(dir->pfsentry); free(dir); return 0; } /* * Opendir replacement, uses a directory listing from the cache if enabled, * otherwise calls original dirent implementation. */ DIR *fscache_opendir(const char *dirname) { struct fsentry key, *list; fscache_DIR *dir; int len; if (!fscache_enabled(dirname)) return dirent_opendir(dirname); /* prepare name (strip trailing '/', replace '.') */ len = strlen(dirname); if ((len == 1 && dirname[0] == '.') || (len && is_dir_sep(dirname[len - 1]))) len--; /* get directory listing from cache */ fsentry_init(&key, NULL, dirname, len); list = fscache_get(&key); if (!list) return NULL; /* alloc and return DIR structure */ dir = (fscache_DIR*) xmalloc(sizeof(fscache_DIR)); dir->base_dir.preaddir = fscache_readdir; dir->base_dir.pclosedir = fscache_closedir; dir->pfsentry = list; return (DIR*) dir; }