"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "src/fsio.c" between
proftpd-1.3.6b.tar.gz and proftpd-1.3.6c.tar.gz

About: ProFTPD is a highly configurable FTP server software (with FTPS and SFTP support).

fsio.c  (proftpd-1.3.6b):fsio.c  (proftpd-1.3.6c)
/* /*
* ProFTPD - FTP server daemon * ProFTPD - FTP server daemon
* Copyright (c) 1997, 1998 Public Flood Software * Copyright (c) 1997, 1998 Public Flood Software
* Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver@tos.net> * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver@tos.net>
* Copyright (c) 2001-2017 The ProFTPD Project * Copyright (c) 2001-2019 The ProFTPD Project
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or * the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
skipping to change at line 1126 skipping to change at line 1126
} }
fsa = *((pr_fs_t **) a); fsa = *((pr_fs_t **) a);
fsb = *((pr_fs_t **) b); fsb = *((pr_fs_t **) b);
return strcmp(fsa->fs_path, fsb->fs_path); return strcmp(fsa->fs_path, fsb->fs_path);
} }
/* Statcache stuff */ /* Statcache stuff */
struct fs_statcache { struct fs_statcache {
xasetmember_t *next, *prev;
pool *sc_pool; pool *sc_pool;
const char *sc_path;
struct stat sc_stat; struct stat sc_stat;
int sc_errno; int sc_errno;
int sc_retval; int sc_retval;
time_t sc_cached_ts; time_t sc_cached_ts;
}; };
struct fs_statcache_evict_data {
time_t now;
time_t max_age;
pr_table_t *cache_tab;
};
static const char *statcache_channel = "fs.statcache"; static const char *statcache_channel = "fs.statcache";
static pool *statcache_pool = NULL; static pool *statcache_pool = NULL;
static unsigned int statcache_size = 0; static unsigned int statcache_size = 0;
static unsigned int statcache_max_age = 0; static unsigned int statcache_max_age = 0;
static unsigned int statcache_flags = 0; static unsigned int statcache_flags = 0;
/* We need to maintain two different caches: one for stat(2) data, and one /* We need to maintain two different caches: one for stat(2) data, and one
* for lstat(2) data. For some files (e.g. symlinks), the struct stat data * for lstat(2) data. For some files (e.g. symlinks), the struct stat data
* for the same path will be different for the two system calls. * for the same path will be different for the two system calls.
*/ */
static pr_table_t *stat_statcache_tab = NULL; static pr_table_t *stat_statcache_tab = NULL;
static xaset_t *stat_statcache_set = NULL;
static pr_table_t *lstat_statcache_tab = NULL; static pr_table_t *lstat_statcache_tab = NULL;
static xaset_t *lstat_statcache_set = NULL;
#define fs_cache_lstat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_LSTAT) #define fs_cache_lstat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_LSTAT)
#define fs_cache_stat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_STAT) #define fs_cache_stat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_STAT)
static const struct fs_statcache *fs_statcache_get(pr_table_t *cache_tab, static const struct fs_statcache *fs_statcache_get(pr_table_t *cache_tab,
const char *path, size_t path_len, time_t now) { xaset_t *cache_set, const char *path, size_t path_len, time_t now) {
const struct fs_statcache *sc = NULL; const struct fs_statcache *sc = NULL;
if (pr_table_count(cache_tab) == 0) { if (pr_table_count(cache_tab) == 0) {
errno = EPERM; errno = EPERM;
return NULL; return NULL;
} }
sc = pr_table_get(cache_tab, path, NULL); sc = pr_table_get(cache_tab, path, NULL);
if (sc != NULL) { if (sc != NULL) {
time_t age; time_t age;
skipping to change at line 1182 skipping to change at line 1180
"using cached entry for '%s' (age %lu %s)", path, "using cached entry for '%s' (age %lu %s)", path,
(unsigned long) age, age != 1 ? "secs" : "sec"); (unsigned long) age, age != 1 ? "secs" : "sec");
return sc; return sc;
} }
pr_trace_msg(statcache_channel, 14, pr_trace_msg(statcache_channel, 14,
"entry for '%s' expired (age %lu %s > max age %lu), removing", path, "entry for '%s' expired (age %lu %s > max age %lu), removing", path,
(unsigned long) age, age != 1 ? "secs" : "sec", (unsigned long) age, age != 1 ? "secs" : "sec",
(unsigned long) statcache_max_age); (unsigned long) statcache_max_age);
(void) pr_table_remove(cache_tab, path, NULL); (void) pr_table_remove(cache_tab, path, NULL);
(void) xaset_remove(cache_set, (xasetmember_t *) sc);
destroy_pool(sc->sc_pool); destroy_pool(sc->sc_pool);
} }
errno = ENOENT; errno = ENOENT;
return NULL; return NULL;
} }
static int fs_statcache_evict_expired(const void *key_data, size_t key_datasz, static int fs_statcache_evict(pr_table_t *cache_tab, xaset_t *cache_set,
const void *value_data, size_t value_datasz, void *user_data) { time_t now) {
const struct fs_statcache *sc;
struct fs_statcache_evict_data *evict_data;
time_t age; time_t age;
pr_table_t *cache_tab = NULL; xasetmember_t *item;
struct fs_statcache *sc;
sc = value_data;
evict_data = user_data;
cache_tab = evict_data->cache_tab; if (cache_set->xas_list == NULL) {
age = evict_data->now - sc->sc_cached_ts; /* Should never happen. */
if (age > evict_data->max_age) { errno = EPERM;
pr_trace_msg(statcache_channel, 14, return -1;
"entry for '%s' expired (age %lu %s > max age %lu), evicting",
(char *) key_data, (unsigned long) age, age != 1 ? "secs" : "sec",
(unsigned long) evict_data->max_age);
(void) pr_table_kremove(cache_tab, key_data, key_datasz, NULL);
destroy_pool(sc->sc_pool);
} }
return 0; /* We only need to remove the FIRST expired item; it should be the first
} * item in the list, since we keep the list in insert (and thus expiry)
* order.
static int fs_statcache_evict(pr_table_t *cache_tab, time_t now) {
int res, table_count;
struct fs_statcache_evict_data evict_data;
/* We try to make room in two passes. First, evict any item that has
* exceeded the maximum age. After that, if we are still not low enough,
* lower the maximum age, and try again. If not enough room by then, then
* we'll try again on the next stat.
*/ */
evict_data.now = now; item = cache_set->xas_list;
evict_data.max_age = statcache_max_age; sc = (struct fs_statcache *) item;
evict_data.cache_tab = cache_tab; age = now - sc->sc_cached_ts;
res = pr_table_do(cache_tab, fs_statcache_evict_expired, &evict_data,
PR_TABLE_DO_FL_ALL);
if (res < 0) {
pr_trace_msg(statcache_channel, 4,
"error evicting expired items: %s", strerror(errno));
}
table_count = pr_table_count(cache_tab);
if (table_count < 0 ||
(unsigned int) table_count < statcache_size) {
return 0;
}
/* Try for a shorter max age. */ if (age < statcache_max_age) {
if (statcache_max_age > 10) { errno = ENOENT;
evict_data.max_age = (statcache_max_age - 10); return -1;
res = pr_table_do(cache_tab, fs_statcache_evict_expired, &evict_data,
PR_TABLE_DO_FL_ALL);
if (res < 0) {
pr_trace_msg(statcache_channel, 4,
"error evicting expired items: %s", strerror(errno));
}
}
table_count = pr_table_count(cache_tab);
if (table_count < 0 ||
(unsigned int) table_count < statcache_size) {
return 0;
} }
pr_trace_msg(statcache_channel, 14, pr_trace_msg(statcache_channel, 14,
"still not enough room in cache (size %d >= max %d)", "entry for '%s' expired (age %lu %s > max age %lu), evicting",
pr_table_count(cache_tab), statcache_size); sc->sc_path, (unsigned long) age, age != 1 ? "secs" : "sec",
errno = EPERM; (unsigned long) statcache_max_age);
return -1;
(void) pr_table_remove(cache_tab, sc->sc_path, NULL);
(void) xaset_remove(cache_set, (xasetmember_t *) sc);
destroy_pool(sc->sc_pool);
return 0;
} }
/* Returns 1 if we successfully added a cache entry, 0 if not, and -1 if /* Returns 1 if we successfully added a cache entry, 0 if not, and -1 if
* there was an error. * there was an error.
*/ */
static int fs_statcache_add(pr_table_t *cache_tab, const char *path, static int fs_statcache_add(pr_table_t *cache_tab, xaset_t *cache_set,
size_t path_len, struct stat *st, int xerrno, int retval, time_t now) { const char *path, size_t path_len, struct stat *st, int xerrno,
int retval, time_t now) {
int res, table_count; int res, table_count;
pool *sc_pool; pool *sc_pool;
struct fs_statcache *sc; struct fs_statcache *sc;
if (statcache_size == 0 || if (statcache_size == 0 ||
statcache_max_age == 0) { statcache_max_age == 0) {
/* Caching disabled; nothing to do here. */ /* Caching disabled; nothing to do here. */
return 0; return 0;
} }
table_count = pr_table_count(cache_tab); table_count = pr_table_count(cache_tab);
if (table_count > 0 && if (table_count > 0 &&
(unsigned int) table_count >= statcache_size) { (unsigned int) table_count >= statcache_size) {
/* We've reached capacity, and need to evict some items to make room. */ /* We've reached capacity, and need to evict an item to make room. */
if (fs_statcache_evict(cache_tab, now) < 0) { if (fs_statcache_evict(cache_tab, cache_set, now) < 0) {
pr_trace_msg(statcache_channel, 8, pr_trace_msg(statcache_channel, 8,
"unable to evict enough items from the cache: %s", strerror(errno)); "unable to evict enough items from the cache: %s", strerror(errno));
} }
/* We did not evict any items, and so are at capacity. */
return 0;
} }
sc_pool = make_sub_pool(statcache_pool); sc_pool = make_sub_pool(statcache_pool);
pr_pool_tag(sc_pool, "FS statcache entry pool"); pr_pool_tag(sc_pool, "FS statcache entry pool");
sc = pcalloc(sc_pool, sizeof(struct fs_statcache)); sc = pcalloc(sc_pool, sizeof(struct fs_statcache));
sc->sc_pool = sc_pool; sc->sc_pool = sc_pool;
sc->sc_path = pstrndup(sc_pool, path, path_len);
memcpy(&(sc->sc_stat), st, sizeof(struct stat)); memcpy(&(sc->sc_stat), st, sizeof(struct stat));
sc->sc_errno = xerrno; sc->sc_errno = xerrno;
sc->sc_retval = retval; sc->sc_retval = retval;
sc->sc_cached_ts = now; sc->sc_cached_ts = now;
res = pr_table_add(cache_tab, pstrndup(sc_pool, path, path_len), sc, res = pr_table_add(cache_tab, sc->sc_path, sc, sizeof(struct fs_statcache *));
sizeof(struct fs_statcache *));
if (res < 0) { if (res < 0) {
int tmp_errno = errno; int tmp_errno = errno;
if (tmp_errno == EEXIST) { if (tmp_errno == EEXIST) {
res = 0; res = 0;
} }
destroy_pool(sc->sc_pool); destroy_pool(sc->sc_pool);
errno = tmp_errno; errno = tmp_errno;
} else {
xaset_insert_end(cache_set, (xasetmember_t *) sc);
} }
return (res == 0 ? 1 : res); return (res == 0 ? 1 : res);
} }
static int cache_stat(pr_fs_t *fs, const char *path, struct stat *st, static int cache_stat(pr_fs_t *fs, const char *path, struct stat *st,
unsigned int op) { unsigned int op) {
int res = -1, retval, xerrno = 0; int res = -1, retval, xerrno = 0;
char cleaned_path[PR_TUNABLE_PATH_MAX+1], pathbuf[PR_TUNABLE_PATH_MAX+1]; char cleaned_path[PR_TUNABLE_PATH_MAX+1], pathbuf[PR_TUNABLE_PATH_MAX+1];
int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL; int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;
size_t path_len; size_t path_len;
pr_table_t *cache_tab = NULL; pr_table_t *cache_tab = NULL;
xaset_t *cache_set = NULL;
const struct fs_statcache *sc = NULL; const struct fs_statcache *sc = NULL;
time_t now; time_t now;
now = time(NULL); now = time(NULL);
memset(cleaned_path, '\0', sizeof(cleaned_path)); memset(cleaned_path, '\0', sizeof(cleaned_path));
memset(pathbuf, '\0', sizeof(pathbuf)); memset(pathbuf, '\0', sizeof(pathbuf));
if (fs->non_std_path == FALSE) { if (fs->non_std_path == FALSE) {
/* Use only absolute path names. Construct them, if given a relative /* Use only absolute path names. Construct them, if given a relative
* path, based on cwd. This obviates the need for something like * path, based on cwd. This obviates the need for something like
skipping to change at line 1367 skipping to change at line 1336
pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0); pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0);
} else { } else {
sstrncpy(cleaned_path, path, sizeof(cleaned_path)-1); sstrncpy(cleaned_path, path, sizeof(cleaned_path)-1);
} }
/* Determine which filesystem function to use, stat() or lstat() */ /* Determine which filesystem function to use, stat() or lstat() */
if (op == FSIO_FILE_STAT) { if (op == FSIO_FILE_STAT) {
mystat = fs->stat ? fs->stat : sys_stat; mystat = fs->stat ? fs->stat : sys_stat;
cache_tab = stat_statcache_tab; cache_tab = stat_statcache_tab;
cache_set = stat_statcache_set;
} else { } else {
mystat = fs->lstat ? fs->lstat : sys_lstat; mystat = fs->lstat ? fs->lstat : sys_lstat;
cache_tab = lstat_statcache_tab; cache_tab = lstat_statcache_tab;
cache_set = lstat_statcache_set;
} }
path_len = strlen(cleaned_path); path_len = strlen(cleaned_path);
sc = fs_statcache_get(cache_tab, cleaned_path, path_len, now); sc = fs_statcache_get(cache_tab, cache_set, cleaned_path, path_len, now);
if (sc != NULL) { if (sc != NULL) {
/* Update the given struct stat pointer with the cached info */ /* Update the given struct stat pointer with the cached info */
memcpy(st, &(sc->sc_stat), sizeof(struct stat)); memcpy(st, &(sc->sc_stat), sizeof(struct stat));
pr_trace_msg(trace_channel, 18, pr_trace_msg(trace_channel, 18,
"using cached stat for %s for path '%s' (retval %d, errno %s)", "using cached stat for %s for path '%s' (retval %d, errno %s)",
op == FSIO_FILE_STAT ? "stat()" : "lstat()", path, sc->sc_retval, op == FSIO_FILE_STAT ? "stat()" : "lstat()", path, sc->sc_retval,
strerror(sc->sc_errno)); strerror(sc->sc_errno));
skipping to change at line 1402 skipping to change at line 1373
pr_trace_msg(trace_channel, 8, "using %s %s for path '%s'", pr_trace_msg(trace_channel, 8, "using %s %s for path '%s'",
fs->fs_name, op == FSIO_FILE_STAT ? "stat()" : "lstat()", path); fs->fs_name, op == FSIO_FILE_STAT ? "stat()" : "lstat()", path);
retval = mystat(fs, cleaned_path, st); retval = mystat(fs, cleaned_path, st);
xerrno = errno; xerrno = errno;
if (retval == 0) { if (retval == 0) {
xerrno = 0; xerrno = 0;
} }
/* Update the cache */ /* Update the cache */
res = fs_statcache_add(cache_tab, cleaned_path, path_len, st, xerrno, retval, res = fs_statcache_add(cache_tab, cache_set, cleaned_path, path_len, st,
now); xerrno, retval, now);
if (res < 0) { if (res < 0) {
pr_trace_msg(trace_channel, 8, pr_trace_msg(trace_channel, 8,
"error adding cached stat for '%s': %s", cleaned_path, strerror(errno)); "error adding cached stat for '%s': %s", cleaned_path, strerror(errno));
} else if (res > 0) { } else if (res > 0) {
pr_trace_msg(trace_channel, 18, pr_trace_msg(trace_channel, 18,
"added cached stat for path '%s' (retval %d, errno %s)", path, "added cached stat for path '%s' (retval %d, errno %s)", path,
retval, strerror(xerrno)); retval, strerror(xerrno));
} }
skipping to change at line 1612 skipping to change at line 1584
if (stat_statcache_tab != NULL) { if (stat_statcache_tab != NULL) {
int size; int size;
size = pr_table_count(stat_statcache_tab); size = pr_table_count(stat_statcache_tab);
pr_trace_msg(statcache_channel, 11, pr_trace_msg(statcache_channel, 11,
"resetting stat(2) statcache (clearing %d %s)", size, "resetting stat(2) statcache (clearing %d %s)", size,
size != 1 ? "entries" : "entry"); size != 1 ? "entries" : "entry");
pr_table_empty(stat_statcache_tab); pr_table_empty(stat_statcache_tab);
pr_table_free(stat_statcache_tab); pr_table_free(stat_statcache_tab);
stat_statcache_tab = NULL; stat_statcache_tab = NULL;
stat_statcache_set = NULL;
} }
if (lstat_statcache_tab != NULL) { if (lstat_statcache_tab != NULL) {
int size; int size;
size = pr_table_count(lstat_statcache_tab); size = pr_table_count(lstat_statcache_tab);
pr_trace_msg(statcache_channel, 11, pr_trace_msg(statcache_channel, 11,
"resetting lstat(2) statcache (clearing %d %s)", size, "resetting lstat(2) statcache (clearing %d %s)", size,
size != 1 ? "entries" : "entry"); size != 1 ? "entries" : "entry");
pr_table_empty(lstat_statcache_tab); pr_table_empty(lstat_statcache_tab);
pr_table_free(lstat_statcache_tab); pr_table_free(lstat_statcache_tab);
lstat_statcache_tab = NULL; lstat_statcache_tab = NULL;
lstat_statcache_set = NULL;
} }
/* Note: we do not need to explicitly destroy each entry in the statcache /* Note: we do not need to explicitly destroy each entry in the statcache
* tables, since ALL entries are allocated out of this statcache_pool. * tables, since ALL entries are allocated out of this statcache_pool.
* And we destroy this pool here. Much easier cleanup that way. * And we destroy this pool here. Much easier cleanup that way.
*/ */
if (statcache_pool != NULL) { if (statcache_pool != NULL) {
destroy_pool(statcache_pool); destroy_pool(statcache_pool);
statcache_pool = NULL; statcache_pool = NULL;
} }
skipping to change at line 1645 skipping to change at line 1619
void pr_fs_statcache_reset(void) { void pr_fs_statcache_reset(void) {
pr_fs_statcache_free(); pr_fs_statcache_free();
if (statcache_pool == NULL) { if (statcache_pool == NULL) {
statcache_pool = make_sub_pool(permanent_pool); statcache_pool = make_sub_pool(permanent_pool);
pr_pool_tag(statcache_pool, "FS Statcache Pool"); pr_pool_tag(statcache_pool, "FS Statcache Pool");
} }
stat_statcache_tab = pr_table_alloc(statcache_pool, 0); stat_statcache_tab = pr_table_alloc(statcache_pool, 0);
stat_statcache_set = xaset_create(statcache_pool, NULL);
lstat_statcache_tab = pr_table_alloc(statcache_pool, 0); lstat_statcache_tab = pr_table_alloc(statcache_pool, 0);
lstat_statcache_set = xaset_create(statcache_pool, NULL);
} }
int pr_fs_statcache_set_policy(unsigned int size, unsigned int max_age, int pr_fs_statcache_set_policy(unsigned int size, unsigned int max_age,
unsigned int flags) { unsigned int flags) {
statcache_size = size; statcache_size = size;
statcache_max_age = max_age; statcache_max_age = max_age;
statcache_flags = flags; statcache_flags = flags;
return 0; return 0;
skipping to change at line 1704 skipping to change at line 1681
pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0); pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0);
res = 0; res = 0;
stat_count = pr_table_exists(stat_statcache_tab, cleaned_path); stat_count = pr_table_exists(stat_statcache_tab, cleaned_path);
if (stat_count > 0) { if (stat_count > 0) {
const struct fs_statcache *sc; const struct fs_statcache *sc;
sc = pr_table_remove(stat_statcache_tab, cleaned_path, NULL); sc = pr_table_remove(stat_statcache_tab, cleaned_path, NULL);
if (sc != NULL) { if (sc != NULL) {
(void) xaset_remove(stat_statcache_set, (xasetmember_t *) sc);
destroy_pool(sc->sc_pool); destroy_pool(sc->sc_pool);
} }
pr_trace_msg(statcache_channel, 17, "cleared stat(2) entry for '%s'", pr_trace_msg(statcache_channel, 17, "cleared stat(2) entry for '%s'",
path); path);
res += stat_count; res += stat_count;
} }
lstat_count = pr_table_exists(lstat_statcache_tab, cleaned_path); lstat_count = pr_table_exists(lstat_statcache_tab, cleaned_path);
if (lstat_count > 0) { if (lstat_count > 0) {
const struct fs_statcache *sc; const struct fs_statcache *sc;
sc = pr_table_remove(lstat_statcache_tab, cleaned_path, NULL); sc = pr_table_remove(lstat_statcache_tab, cleaned_path, NULL);
if (sc != NULL) { if (sc != NULL) {
(void) xaset_remove(lstat_statcache_set, (xasetmember_t *) sc);
destroy_pool(sc->sc_pool); destroy_pool(sc->sc_pool);
} }
pr_trace_msg(statcache_channel, 17, "cleared lstat(2) entry for '%s'", pr_trace_msg(statcache_channel, 17, "cleared lstat(2) entry for '%s'",
path); path);
res += lstat_count; res += lstat_count;
} }
} else { } else {
/* Caller is requesting that we empty the entire cache. */ /* Caller is requesting that we empty the entire cache. */
skipping to change at line 7226 skipping to change at line 7205
} else { } else {
pr_fsio_chdir("/", FALSE); pr_fsio_chdir("/", FALSE);
pr_fs_setcwd("/"); pr_fs_setcwd("/");
} }
/* Prepare the stat cache as well. */ /* Prepare the stat cache as well. */
statcache_pool = make_sub_pool(permanent_pool); statcache_pool = make_sub_pool(permanent_pool);
pr_pool_tag(statcache_pool, "FS Statcache Pool"); pr_pool_tag(statcache_pool, "FS Statcache Pool");
stat_statcache_tab = pr_table_alloc(statcache_pool, 0); stat_statcache_tab = pr_table_alloc(statcache_pool, 0);
stat_statcache_set = xaset_create(statcache_pool, NULL);
lstat_statcache_tab = pr_table_alloc(statcache_pool, 0); lstat_statcache_tab = pr_table_alloc(statcache_pool, 0);
lstat_statcache_set = xaset_create(statcache_pool, NULL);
return 0; return 0;
} }
#ifdef PR_USE_DEVEL #ifdef PR_USE_DEVEL
static const char *get_fs_hooks_str(pool *p, pr_fs_t *fs) { static const char *get_fs_hooks_str(pool *p, pr_fs_t *fs) {
char *hooks = ""; char *hooks = "";
if (fs->stat) { if (fs->stat) {
 End of changes. 34 change blocks. 
80 lines changed or deleted 60 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)