• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /asuswrt-rt-n18u-9.0.0.4.380.2695/release/src-rt-6.x.4708/router/lighttpd-1.4.39/src/
1#include "log.h"
2#include "stat_cache.h"
3#include "fdevent.h"
4#include "etag.h"
5
6#include <sys/types.h>
7#include <sys/stat.h>
8
9#include <stdlib.h>
10#include <string.h>
11#include <errno.h>
12#include <unistd.h>
13#include <stdio.h>
14#include <fcntl.h>
15#include <assert.h>
16
17#ifdef HAVE_ATTR_ATTRIBUTES_H
18# include <attr/attributes.h>
19#endif
20
21#ifdef HAVE_SYS_EXTATTR_H
22# include <sys/extattr.h>
23#endif
24
25#ifdef HAVE_LIBSMBCLIENT
26#include <libsmbclient.h>
27#endif
28
29#ifdef HAVE_FAM_H
30# include <fam.h>
31#endif
32
33#include "sys-mmap.h"
34
35/* NetBSD 1.3.x needs it */
36#ifndef MAP_FAILED
37# define MAP_FAILED -1
38#endif
39
40#ifndef O_LARGEFILE
41# define O_LARGEFILE 0
42#endif
43
44#ifndef HAVE_LSTAT
45# define lstat stat
46#endif
47
48#define DBE 0
49
50#if 0
51/* enables debug code for testing if all nodes in the stat-cache as accessable */
52#define DEBUG_STAT_CACHE
53#endif
54
55/*
56 * stat-cache
57 *
58 * we cache the stat() calls in our own storage
59 * the directories are cached in FAM
60 *
61 * if we get a change-event from FAM, we increment the version in the FAM->dir mapping
62 *
63 * if the stat()-cache is queried we check if the version id for the directory is the
64 * same and return immediatly.
65 *
66 *
67 * What we need:
68 *
69 * - for each stat-cache entry we need a fast indirect lookup on the directory name
70 * - for each FAMRequest we have to find the version in the directory cache (index as userdata)
71 *
72 * stat <<-> directory <-> FAMRequest
73 *
74 * if file is deleted, directory is dirty, file is rechecked ...
75 * if directory is deleted, directory mapping is removed
76 *
77 * */
78
79#ifdef HAVE_FAM_H
80typedef struct {
81	FAMRequest *req;
82
83	buffer *name;
84
85	int version;
86} fam_dir_entry;
87#endif
88
89/* the directory name is too long to always compare on it
90 * - we need a hash
91 * - the hash-key is used as sorting criteria for a tree
92 * - a splay-tree is used as we can use the caching effect of it
93 */
94
95/* we want to cleanup the stat-cache every few seconds, let's say 10
96 *
97 * - remove entries which are outdated since 30s
98 * - remove entries which are fresh but havn't been used since 60s
99 * - if we don't have a stat-cache entry for a directory, release it from the monitor
100 */
101
102#ifdef DEBUG_STAT_CACHE
103typedef struct {
104	int *ptr;
105
106	size_t used;
107	size_t size;
108} fake_keys;
109
110static fake_keys ctrl;
111#endif
112
113stat_cache *stat_cache_init(void) {
114	stat_cache *sc = NULL;
115
116	sc = calloc(1, sizeof(*sc));
117
118	sc->dir_name = buffer_init();
119	sc->hash_key = buffer_init();
120
121#ifdef HAVE_FAM_H
122	sc->fam_fcce_ndx = -1;
123#endif
124
125#ifdef DEBUG_STAT_CACHE
126	ctrl.size = 0;
127#endif
128
129	return sc;
130}
131
132static stat_cache_entry * stat_cache_entry_init(void) {
133	stat_cache_entry *sce = NULL;
134
135	sce = calloc(1, sizeof(*sce));
136
137	sce->name = buffer_init();
138	sce->etag = buffer_init();
139	sce->content_type = buffer_init();
140
141	return sce;
142}
143
144static void stat_cache_entry_free(void *data) {
145	stat_cache_entry *sce = data;
146	if (!sce) return;
147
148	buffer_free(sce->etag);
149	buffer_free(sce->name);
150	buffer_free(sce->content_type);
151
152	free(sce);
153}
154
155#ifdef HAVE_FAM_H
156static fam_dir_entry * fam_dir_entry_init(void) {
157	fam_dir_entry *fam_dir = NULL;
158
159	fam_dir = calloc(1, sizeof(*fam_dir));
160
161	fam_dir->name = buffer_init();
162
163	return fam_dir;
164}
165
166static void fam_dir_entry_free(FAMConnection *fc, void *data) {
167	fam_dir_entry *fam_dir = data;
168
169	if (!fam_dir) return;
170
171	FAMCancelMonitor(fc, fam_dir->req);
172
173	buffer_free(fam_dir->name);
174	free(fam_dir->req);
175
176	free(fam_dir);
177}
178#endif
179
180void stat_cache_free(stat_cache *sc) {
181	while (sc->files) {
182		int osize;
183		splay_tree *node = sc->files;
184
185		osize = sc->files->size;
186
187		stat_cache_entry_free(node->data);
188		sc->files = splaytree_delete(sc->files, node->key);
189
190		force_assert(osize - 1 == splaytree_size(sc->files));
191	}
192
193	buffer_free(sc->dir_name);
194	buffer_free(sc->hash_key);
195
196#ifdef HAVE_FAM_H
197	while (sc->dirs) {
198		int osize;
199		splay_tree *node = sc->dirs;
200
201		osize = sc->dirs->size;
202
203		fam_dir_entry_free(&sc->fam, node->data);
204		sc->dirs = splaytree_delete(sc->dirs, node->key);
205
206		if (osize == 1) {
207			force_assert(NULL == sc->dirs);
208		} else {
209			force_assert(osize == (sc->dirs->size + 1));
210		}
211	}
212
213	if (-1 != sc->fam_fcce_ndx) {
214		/* fd events already gone */
215		sc->fam_fcce_ndx = -1;
216
217		FAMClose(&sc->fam);
218	}
219#endif
220	free(sc);
221}
222
223#if defined(HAVE_XATTR)
224static int stat_cache_attr_get(buffer *buf, char *name) {
225	int attrlen;
226	int ret;
227
228	buffer_string_prepare_copy(buf, 1023);
229	attrlen = buf->size - 1;
230	if(0 == (ret = attr_get(name, "Content-Type", buf->ptr, &attrlen, 0))) {
231		buffer_commit(buf, attrlen);
232	}
233	return ret;
234}
235#elif defined(HAVE_EXTATTR)
236static int stat_cache_attr_get(buffer *buf, char *name) {
237	ssize_t attrlen;
238
239	buffer_string_prepare_copy(buf, 1023);
240
241	if (-1 != (attrlen = extattr_get_file(name, EXTATTR_NAMESPACE_USER, "Content-Type", buf->ptr, buf->size - 1))) {
242		buf->used = attrlen + 1;
243		buf->ptr[attrlen] = '\0';
244		return 0;
245	}
246	return -1;
247}
248#endif
249
250/* the famous DJB hash function for strings */
251static uint32_t hashme(buffer *str) {
252	uint32_t hash = 5381;
253	const char *s;
254	for (s = str->ptr; *s; s++) {
255		hash = ((hash << 5) + hash) + *s;
256	}
257
258	hash &= ~(((uint32_t)1) << 31); /* strip the highest bit */
259
260	return hash;
261}
262
263#ifdef HAVE_FAM_H
264handler_t stat_cache_handle_fdevent(server *srv, void *_fce, int revent) {
265	size_t i;
266	stat_cache *sc = srv->stat_cache;
267	size_t events;
268
269	UNUSED(_fce);
270	/* */
271
272	if (revent & FDEVENT_IN) {
273		events = FAMPending(&sc->fam);
274
275		for (i = 0; i < events; i++) {
276			FAMEvent fe;
277			fam_dir_entry *fam_dir;
278			splay_tree *node;
279			int ndx, j;
280
281			FAMNextEvent(&sc->fam, &fe);
282
283			/* handle event */
284
285			switch(fe.code) {
286			case FAMChanged:
287			case FAMDeleted:
288			case FAMMoved:
289				/* if the filename is a directory remove the entry */
290
291				fam_dir = fe.userdata;
292				fam_dir->version++;
293
294				/* file/dir is still here */
295				if (fe.code == FAMChanged) break;
296
297				/* we have 2 versions, follow and no-follow-symlink */
298
299				for (j = 0; j < 2; j++) {
300					buffer_copy_string(sc->hash_key, fe.filename);
301					buffer_append_int(sc->hash_key, j);
302
303					ndx = hashme(sc->hash_key);
304
305					sc->dirs = splaytree_splay(sc->dirs, ndx);
306					node = sc->dirs;
307
308					if (node && (node->key == ndx)) {
309						int osize = splaytree_size(sc->dirs);
310
311						fam_dir_entry_free(&sc->fam, node->data);
312						sc->dirs = splaytree_delete(sc->dirs, ndx);
313
314						force_assert(osize - 1 == splaytree_size(sc->dirs));
315					}
316				}
317				break;
318			default:
319				break;
320			}
321		}
322	}
323
324	if (revent & FDEVENT_HUP) {
325		/* fam closed the connection */
326		fdevent_event_del(srv->ev, &(sc->fam_fcce_ndx), FAMCONNECTION_GETFD(&sc->fam));
327		fdevent_unregister(srv->ev, FAMCONNECTION_GETFD(&sc->fam));
328
329		FAMClose(&sc->fam);
330	}
331
332	return HANDLER_GO_ON;
333}
334
335static int buffer_copy_dirname(buffer *dst, buffer *file) {
336	size_t i;
337
338	if (buffer_string_is_empty(file)) return -1;
339
340	for (i = buffer_string_length(file); i > 0; i--) {
341		if (file->ptr[i] == '/') {
342			buffer_copy_string_len(dst, file->ptr, i);
343			return 0;
344		}
345	}
346
347	return -1;
348}
349#endif
350
351#ifdef HAVE_LSTAT
352static int stat_cache_lstat(server *srv, buffer *dname, struct stat *lst) {
353	if (lstat(dname->ptr, lst) == 0) {
354		return S_ISLNK(lst->st_mode) ? 0 : 1;
355	}
356	else {
357		log_error_write(srv, __FILE__, __LINE__, "sbs",
358				"lstat failed for:",
359				dname, strerror(errno));
360	};
361	return -1;
362}
363#endif
364
365/***
366 *
367 *
368 *
369 * returns:
370 *  - HANDLER_FINISHED on cache-miss (don't forget to reopen the file)
371 *  - HANDLER_ERROR on stat() failed -> see errno for problem
372 */
373
374handler_t stat_cache_get_entry(server *srv, connection *con, buffer *name, stat_cache_entry **ret_sce) {
375#ifdef HAVE_FAM_H
376	fam_dir_entry *fam_dir = NULL;
377	int dir_ndx = -1;
378#endif
379	stat_cache_entry *sce = NULL;
380	stat_cache *sc;
381	struct stat st;
382	size_t k;
383	int fd;
384	struct stat lst;
385#ifdef DEBUG_STAT_CACHE
386	size_t i;
387#endif
388
389	int file_ndx;
390
391	*ret_sce = NULL;
392
393	/*
394	 * check if the directory for this file has changed
395	 */
396
397	sc = srv->stat_cache;
398
399	buffer_copy_buffer(sc->hash_key, name);
400	buffer_append_int(sc->hash_key, con->conf.follow_symlink);
401
402	file_ndx = hashme(sc->hash_key);
403	sc->files = splaytree_splay(sc->files, file_ndx);
404
405#ifdef DEBUG_STAT_CACHE
406	for (i = 0; i < ctrl.used; i++) {
407		if (ctrl.ptr[i] == file_ndx) break;
408	}
409#endif
410
411	if (sc->files && (sc->files->key == file_ndx)) {
412#ifdef DEBUG_STAT_CACHE
413		/* it was in the cache */
414		force_assert(i < ctrl.used);
415#endif
416
417		/* we have seen this file already and
418		 * don't stat() it again in the same second */
419
420		sce = sc->files->data;
421
422		/* check if the name is the same, we might have a collision */
423
424		if (buffer_is_equal(name, sce->name)) {
425			if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_SIMPLE) {
426				if (sce->stat_ts == srv->cur_ts) {
427					*ret_sce = sce;
428					return HANDLER_GO_ON;
429				}
430			}
431		} else {
432			/* collision, forget about the entry */
433			sce = NULL;
434		}
435	} else {
436#ifdef DEBUG_STAT_CACHE
437		if (i != ctrl.used) {
438			log_error_write(srv, __FILE__, __LINE__, "xSB",
439				file_ndx, "was already inserted but not found in cache, ", name);
440		}
441		force_assert(i == ctrl.used);
442#endif
443	}
444
445#ifdef HAVE_FAM_H
446	/* dir-check */
447	if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
448		if (0 != buffer_copy_dirname(sc->dir_name, name)) {
449			log_error_write(srv, __FILE__, __LINE__, "sb",
450				"no '/' found in filename:", name);
451			return HANDLER_ERROR;
452		}
453
454		buffer_copy_buffer(sc->hash_key, sc->dir_name);
455		buffer_append_int(sc->hash_key, con->conf.follow_symlink);
456
457		dir_ndx = hashme(sc->hash_key);
458
459		sc->dirs = splaytree_splay(sc->dirs, dir_ndx);
460
461		if ((NULL != sc->dirs) && (sc->dirs->key == dir_ndx)) {
462			fam_dir = sc->dirs->data;
463
464			/* check whether we got a collision */
465			if (buffer_is_equal(sc->dir_name, fam_dir->name)) {
466				/* test whether a found file cache entry is still ok */
467				if ((NULL != sce) && (fam_dir->version == sce->dir_version)) {
468					/* the stat()-cache entry is still ok */
469
470					*ret_sce = sce;
471					return HANDLER_GO_ON;
472				}
473			} else {
474				/* hash collision, forget about the entry */
475				fam_dir = NULL;
476			}
477		}
478	}
479#endif
480
481	/*
482	 * *lol*
483	 * - open() + fstat() on a named-pipe results in a (intended) hang.
484	 * - stat() if regular file + open() to see if we can read from it is better
485	 *
486	 * */
487	int r;
488
489	//- Sungmin add
490#ifdef HAVE_LIBSMBCLIENT
491	if(con->mode == DIRECT)
492		r =  stat(name->ptr, &st);
493	else if(con->mode == SMB_NTLM||con->mode == SMB_BASIC)
494		r = smbc_wrapper_stat(con, name->ptr, &st);
495#else
496	r =  stat(name->ptr, &st);
497#endif
498
499	if (r == -1) {
500		return HANDLER_ERROR;
501	}
502
503
504	if (S_ISREG(st.st_mode)) {
505		/* fix broken stat/open for symlinks to reg files with appended slash on freebsd,osx */
506		if (name->ptr[buffer_string_length(name) - 1] == '/') {
507			errno = ENOTDIR;
508			return HANDLER_ERROR;
509		}
510
511		/* try to open the file to check if we can read it */
512		//- Sungmin add
513#ifdef HAVE_LIBSMBCLIENT
514		if(con->mode == DIRECT) {
515			fd = open(name->ptr, O_RDONLY);
516			if(fd == -1) {
517				return HANDLER_ERROR;
518			}
519			close(fd);
520		}
521		else if(con->mode == SMB_NTLM) {
522			smb_file_t *fp;
523			fp = smbc_cli_ntcreate(con->smb_info->cli, name->ptr,
524				FILE_READ_DATA | FILE_WRITE_DATA, FILE_OPEN, 0);
525			if(fp == NULL) {
526				return HANDLER_ERROR;
527			}
528			smbc_cli_close(con->smb_info->cli, fp);
529		}
530		else {
531			//Cdbg(DBE, "call smbc_open..%s", con->smb_info->url.path->ptr);
532			//Cdbg(DBE, "call smbc_open..%s", name->ptr);
533
534			//fd = smbc_open(con->smb_info->url.path->ptr, O_RDONLY, 0);
535			fd = smbc_open(name->ptr, O_RDONLY, 0);
536
537			if(fd == -1) {
538				Cdbg(DBE, "fail to smbc_open");
539				return HANDLER_ERROR;
540			}
541			//Cdbg(DBE, "call smbc_close");
542			smbc_close(fd);
543		}
544#else
545		if (-1 == (fd = open(name->ptr, O_RDONLY))) {
546			return HANDLER_ERROR;
547		}
548		close(fd);
549#endif
550	}
551
552	if (NULL == sce) {
553
554		sce = stat_cache_entry_init();
555		buffer_copy_buffer(sce->name, name);
556
557		/* already splayed file_ndx */
558		if ((NULL != sc->files) && (sc->files->key == file_ndx)) {
559			/* hash collision: replace old entry */
560			stat_cache_entry_free(sc->files->data);
561			sc->files->data = sce;
562		} else {
563			int osize = splaytree_size(sc->files);
564
565			sc->files = splaytree_insert(sc->files, file_ndx, sce);
566			force_assert(osize + 1 == splaytree_size(sc->files));
567
568#ifdef DEBUG_STAT_CACHE
569			if (ctrl.size == 0) {
570				ctrl.size = 16;
571				ctrl.used = 0;
572				ctrl.ptr = malloc(ctrl.size * sizeof(*ctrl.ptr));
573			} else if (ctrl.size == ctrl.used) {
574				ctrl.size += 16;
575				ctrl.ptr = realloc(ctrl.ptr, ctrl.size * sizeof(*ctrl.ptr));
576			}
577
578			ctrl.ptr[ctrl.used++] = file_ndx;
579#endif
580		}
581		force_assert(sc->files);
582		force_assert(sc->files->data == sce);
583	}
584
585	sce->st = st;
586	sce->stat_ts = srv->cur_ts;
587
588	/* catch the obvious symlinks
589	 *
590	 * this is not a secure check as we still have a race-condition between
591	 * the stat() and the open. We can only solve this by
592	 * 1. open() the file
593	 * 2. fstat() the fd
594	 *
595	 * and keeping the file open for the rest of the time. But this can
596	 * only be done at network level.
597	 *
598	 * per default it is not a symlink
599	 * */
600#ifdef HAVE_LSTAT
601	sce->is_symlink = 0;
602
603	/* we want to only check for symlinks if we should block symlinks.
604	 */
605	if (!con->conf.follow_symlink) {
606		if (stat_cache_lstat(srv, name, &lst)  == 0) {
607#ifdef DEBUG_STAT_CACHE
608				log_error_write(srv, __FILE__, __LINE__, "sb",
609						"found symlink", name);
610#endif
611				sce->is_symlink = 1;
612		}
613
614		/*
615		 * we assume "/" can not be symlink, so
616		 * skip the symlink stuff if our path is /
617		 **/
618		else if (buffer_string_length(name) > 1) {
619			buffer *dname;
620			char *s_cur;
621
622			dname = buffer_init();
623			buffer_copy_buffer(dname, name);
624
625			while ((s_cur = strrchr(dname->ptr, '/'))) {
626				buffer_string_set_length(dname, s_cur - dname->ptr);
627				if (dname->ptr == s_cur) {
628#ifdef DEBUG_STAT_CACHE
629					log_error_write(srv, __FILE__, __LINE__, "s", "reached /");
630#endif
631					break;
632				}
633#ifdef DEBUG_STAT_CACHE
634				log_error_write(srv, __FILE__, __LINE__, "sbs",
635						"checking if", dname, "is a symlink");
636#endif
637				if (stat_cache_lstat(srv, dname, &lst)  == 0) {
638					sce->is_symlink = 1;
639#ifdef DEBUG_STAT_CACHE
640					log_error_write(srv, __FILE__, __LINE__, "sb",
641							"found symlink", dname);
642#endif
643					break;
644				};
645			};
646			buffer_free(dname);
647		};
648	};
649#endif
650
651	if (S_ISREG(st.st_mode)) {
652		/* determine mimetype */
653		buffer_reset(sce->content_type);
654#if defined(HAVE_XATTR) || defined(HAVE_EXTATTR)
655		if (con->conf.use_xattr) {
656			stat_cache_attr_get(sce->content_type, name->ptr);
657		}
658#endif
659		/* xattr did not set a content-type. ask the config */
660		if (buffer_string_is_empty(sce->content_type)) {
661			size_t namelen = buffer_string_length(name);
662
663			for (k = 0; k < con->conf.mimetypes->used; k++) {
664				data_string *ds = (data_string *)con->conf.mimetypes->data[k];
665				buffer *type = ds->key;
666				size_t typelen = buffer_string_length(type);
667
668				if (buffer_is_empty(type)) continue;
669
670				/* check if the right side is the same */
671				if (typelen > namelen) continue;
672
673				if (0 == strncasecmp(name->ptr + namelen - typelen, type->ptr, typelen)) {
674					buffer_copy_buffer(sce->content_type, ds->value);
675					break;
676				}
677			}
678		}
679		etag_create(sce->etag, &(sce->st), con->etag_flags);
680	} else if (S_ISDIR(st.st_mode)) {
681		etag_create(sce->etag, &(sce->st), con->etag_flags);
682	}
683
684#ifdef HAVE_FAM_H
685	if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
686		/* is this directory already registered ? */
687		if (NULL == fam_dir) {
688			fam_dir = fam_dir_entry_init();
689
690			buffer_copy_buffer(fam_dir->name, sc->dir_name);
691
692			fam_dir->version = 1;
693
694			fam_dir->req = calloc(1, sizeof(FAMRequest));
695
696			if (0 != FAMMonitorDirectory(&sc->fam, fam_dir->name->ptr,
697						     fam_dir->req, fam_dir)) {
698
699				log_error_write(srv, __FILE__, __LINE__, "sbsbs",
700						"monitoring dir failed:",
701						fam_dir->name,
702						"file:", name,
703						FamErrlist[FAMErrno]);
704
705				fam_dir_entry_free(&sc->fam, fam_dir);
706				fam_dir = NULL;
707			} else {
708				int osize = splaytree_size(sc->dirs);
709
710				/* already splayed dir_ndx */
711				if ((NULL != sc->dirs) && (sc->dirs->key == dir_ndx)) {
712					/* hash collision: replace old entry */
713					fam_dir_entry_free(&sc->fam, sc->dirs->data);
714					sc->dirs->data = fam_dir;
715				} else {
716					sc->dirs = splaytree_insert(sc->dirs, dir_ndx, fam_dir);
717					force_assert(osize == (splaytree_size(sc->dirs) - 1));
718				}
719
720				force_assert(sc->dirs);
721				force_assert(sc->dirs->data == fam_dir);
722			}
723		}
724
725		/* bind the fam_fc to the stat() cache entry */
726
727		if (fam_dir) {
728			sce->dir_version = fam_dir->version;
729		}
730	}
731#endif
732
733	*ret_sce = sce;
734
735	return HANDLER_GO_ON;
736}
737
738/**
739 * remove stat() from cache which havn't been stat()ed for
740 * more than 10 seconds
741 *
742 *
743 * walk though the stat-cache, collect the ids which are too old
744 * and remove them in a second loop
745 */
746
747static int stat_cache_tag_old_entries(server *srv, splay_tree *t, int *keys, size_t *ndx) {
748	stat_cache_entry *sce;
749
750	if (!t) return 0;
751
752	stat_cache_tag_old_entries(srv, t->left, keys, ndx);
753	stat_cache_tag_old_entries(srv, t->right, keys, ndx);
754
755	sce = t->data;
756
757	if (srv->cur_ts - sce->stat_ts > 2) {
758		keys[(*ndx)++] = t->key;
759	}
760
761	return 0;
762}
763
764int stat_cache_trigger_cleanup(server *srv) {
765	stat_cache *sc;
766	size_t max_ndx = 0, i;
767	int *keys;
768
769	sc = srv->stat_cache;
770
771	if (!sc->files) return 0;
772
773	keys = calloc(1, sizeof(int) * sc->files->size);
774
775	stat_cache_tag_old_entries(srv, sc->files, keys, &max_ndx);
776
777	for (i = 0; i < max_ndx; i++) {
778		int ndx = keys[i];
779		splay_tree *node;
780
781		sc->files = splaytree_splay(sc->files, ndx);
782
783		node = sc->files;
784
785		if (node && (node->key == ndx)) {
786#ifdef DEBUG_STAT_CACHE
787			size_t j;
788			int osize = splaytree_size(sc->files);
789			stat_cache_entry *sce = node->data;
790#endif
791			stat_cache_entry_free(node->data);
792			sc->files = splaytree_delete(sc->files, ndx);
793
794#ifdef DEBUG_STAT_CACHE
795			for (j = 0; j < ctrl.used; j++) {
796				if (ctrl.ptr[j] == ndx) {
797					ctrl.ptr[j] = ctrl.ptr[--ctrl.used];
798					break;
799				}
800			}
801
802			force_assert(osize - 1 == splaytree_size(sc->files));
803#endif
804		}
805	}
806
807	free(keys);
808
809	return 0;
810}
811