source: trunk/src/sh_fInotify.c@ 377

Last change on this file since 377 was 376, checked in by katerina, 13 years ago

Fix some repetitive error messages.

File size: 17.8 KB
RevLine 
[368]1/* SAMHAIN file system integrity testing */
2/* Copyright (C) 2011 Rainer Wichmann */
3/* */
4/* This program is free software; you can redistribute it */
5/* and/or modify */
6/* it under the terms of the GNU General Public License as */
7/* published by */
8/* the Free Software Foundation; either version 2 of the License, or */
9/* (at your option) any later version. */
10/* */
11/* This program is distributed in the hope that it will be useful, */
12/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
13/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
14/* GNU General Public License for more details. */
15/* */
16/* You should have received a copy of the GNU General Public License */
17/* along with this program; if not, write to the Free Software */
18/* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
19
20/***************************************************************************
21 *
22 * This file provides a module for samhain to use inotify for file checking.
23 *
24 */
25
26#include "config_xor.h"
27
28#if (defined(SH_WITH_CLIENT) || defined(SH_STANDALONE))
29
30#include "samhain.h"
31#include "sh_utils.h"
32#include "sh_modules.h"
33#include "sh_pthread.h"
34#include "sh_inotify.h"
35#include "sh_unix.h"
36#include "sh_hash.h"
37#include "sh_files.h"
38#include "sh_ignore.h"
39
40#define FIL__ _("sh_fInotify.c")
41
42sh_watches sh_file_watches = SH_INOTIFY_INITIALIZER;
43
[373]44#if defined(HAVE_SYS_INOTIFY_H)
45
[368]46static sh_watches sh_file_missing = SH_INOTIFY_INITIALIZER;
47
48#include <sys/inotify.h>
49
50/* --- Configuration ------- */
51
52static int ShfInotifyActive = S_FALSE;
53
54static unsigned long ShfInotifyWatches = 0;
55
56static int sh_fInotify_active(const char *s)
57{
58 int value;
59
60 SL_ENTER(_("sh_fInotify_active"));
61 value = sh_util_flagval(s, &ShfInotifyActive);
62 if (value == 0 && ShfInotifyActive != S_FALSE)
63 {
64 sh.flag.inotify |= SH_INOTIFY_USE;
65 sh.flag.inotify |= SH_INOTIFY_DOSCAN;
66 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
67 }
68 if (value == 0 && ShfInotifyActive == S_FALSE)
69 {
70 sh.flag.inotify = 0;
71 }
72 SL_RETURN((value), _("sh_fInotify_active"));
73}
74
75static int sh_fInotify_watches(const char *s)
76{
77 int retval = -1;
78 char * foo;
79 unsigned long value;
80
81 SL_ENTER(_("sh_fInotify_watches"));
82
83 value = strtoul(s, &foo, 0);
84 if (*foo == '\0')
85 {
86 ShfInotifyWatches = (value > 2147483647) ? 2147483647 /* MAX_INT_32 */: value;
87 retval = 0;
88 }
89 SL_RETURN((retval), _("sh_fInotify_watches"));
90}
91
92
93sh_rconf sh_fInotify_table[] = {
94 {
95 N_("inotifyactive"),
96 sh_fInotify_active,
97 },
98 {
99 N_("inotifywatches"),
100 sh_fInotify_watches,
101 },
102 {
103 NULL,
104 NULL
105 }
106};
107
108/* --- End Configuration --- */
109
110static int sh_fInotify_init_internal(void);
111static int sh_fInotify_process(struct inotify_event * event);
112static int sh_fInotify_report(struct inotify_event * event, char * filename,
[373]113 int class, unsigned long check_mask, int ftype, int rdepth);
[368]114
115int sh_fInotify_init(struct mod_type * arg)
116{
117#ifndef HAVE_PTHREAD
118 (void) arg;
119 return SH_MOD_FAILED;
120#else
121
122 if (ShfInotifyActive == S_FALSE)
123 return SH_MOD_FAILED;
124
125 if (sh.flag.checkSum == SH_CHECK_INIT)
126 return SH_MOD_FAILED;
127
128 if (arg != NULL && arg->initval < 0 &&
129 (sh.flag.isdaemon == S_TRUE || sh.flag.loop == S_TRUE))
130 {
131 /* Init from main thread */
132 sh.flag.inotify |= SH_INOTIFY_DOSCAN;
133 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
134
135 if (0 == sh_pthread_create(sh_threaded_module_run, (void *)arg))
136 {
137 return SH_MOD_THREAD;
138 }
139 else
140 {
141 sh.flag.inotify = 0;
142 return SH_MOD_FAILED;
143 }
144 }
[371]145 else if (arg != NULL && arg->initval < 0 &&
146 (sh.flag.isdaemon != S_TRUE && sh.flag.loop != S_TRUE))
147 {
148 sh.flag.inotify = 0;
149 return SH_MOD_FAILED;
150 }
[368]151 else if (arg != NULL && arg->initval == SH_MOD_THREAD &&
152 (sh.flag.isdaemon == S_TRUE || sh.flag.loop == S_TRUE))
153 {
154 /* Reconfigure from main thread */
155 /* sh_fInotify_init_internal(); */
156 sh.flag.inotify |= SH_INOTIFY_DOSCAN;
157 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
158 return SH_MOD_THREAD;
159 }
160
161 /* Within thread, init */
162 return sh_fInotify_init_internal();
163#endif
164}
165
166int sh_fInotify_run()
167{
168 ssize_t len = -1;
169 char * buffer = SH_ALLOC(16384);
[372]170 static int count = 0;
171 static int count2 = 0;
[368]172
173 if (ShfInotifyActive == S_FALSE)
174 return SH_MOD_FAILED;
175
176 if ( (sh.flag.inotify & SH_INOTIFY_DOSCAN) ||
177 (sh.flag.inotify & SH_INOTIFY_NEEDINIT))
178 {
179 if (0 != sh_fInotify_init_internal())
180 return SH_MOD_FAILED;
181 }
182
183 /* Blocking read from inotify file descriptor.
184 */
[371]185 len = sh_inotify_read_timeout(buffer, 16384, 1);
[368]186
187 if (len > 0)
188 {
189 struct inotify_event *event;
190 int i = 0;
191
192 while (i < len)
193 {
194 event = (struct inotify_event *) &(buffer[i]);
195
196 sh_fInotify_process(event);
197
198 i += sizeof (struct inotify_event) + event->len;
199 }
200
201 if ( (sh.flag.inotify & SH_INOTIFY_DOSCAN) ||
202 (sh.flag.inotify & SH_INOTIFY_NEEDINIT))
203 {
204 if (0 != sh_fInotify_init_internal())
205 return SH_MOD_FAILED;
206 }
[373]207 }
[368]208
209 /* Re-scan 'dormant' list of sh_file_missing.
210 */
211 sh_inotify_recheck_watches (&sh_file_watches, &sh_file_missing);
212
[372]213 ++count;
214 ++count2;
[371]215
216 if (count >= 10)
217 {
[372]218 count = 0; /* Re-expand glob patterns to discover added files. */
[373]219 sh.flag.inotify |= SH_INOTIFY_INSCAN;
[371]220 sh_files_check_globFilePatterns();
[373]221 sh.flag.inotify &= ~SH_INOTIFY_INSCAN;
222 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
[371]223 }
224
[372]225 if (count2 >= 300)
226 {
227 count2 = 0; /* Update baseline database. */
[373]228 if (sh.flag.checkSum == SH_CHECK_CHECK && sh.flag.update == S_TRUE)
[372]229 sh_hash_writeout ();
230 }
231
[368]232 return 0;
233}
234
235/* We block in the read() call on the inotify descriptor,
236 * so we always run.
237 */
238int sh_fInotify_timer(time_t tcurrent)
239{
240 (void) tcurrent;
241 return 1;
242}
243
244int sh_fInotify_cleanup()
245{
246 sh_inotify_purge_dormant(&sh_file_watches);
247 sh_inotify_remove(&sh_file_watches);
248 sh_inotify_init(&sh_file_watches);
249 return 0;
250}
251
252int sh_fInotify_reconf()
253{
254 sh.flag.inotify = 0;
255
256 ShfInotifyWatches = 0;
257 ShfInotifyActive = 0;
258
259 return sh_fInotify_cleanup();
260}
261
262#define PROC_WATCHES_MAX _("/proc/sys/fs/inotify/max_user_watches")
263
264static void sh_fInotify_set_nwatches()
265{
[376]266 static int fails = 0;
267
268 if (ShfInotifyWatches == 0 || fails == 1)
[368]269 return;
270
271 if (0 == access(PROC_WATCHES_MAX, R_OK|W_OK)) /* flawfinder: ignore */
272 {
273 FILE * fd;
274
275 if (NULL != (fd = fopen(PROC_WATCHES_MAX, "r+")))
276 {
277 char str[128];
278 char * ret;
279 char * ptr;
280 unsigned long wn;
281
282 str[0] = '\0';
283 ret = fgets(str, 128, fd);
284 if (ret && *str != '\0')
285 {
286 wn = strtoul(str, &ptr, 0);
287 if (*ptr == '\0' || *ptr == '\n')
288 {
289 if (wn < ShfInotifyWatches)
290 {
291 sl_snprintf(str, sizeof(str), "%lu\n", ShfInotifyWatches);
292 (void) fseek(fd, 0L, SEEK_SET);
293 fputs(str, fd);
294 }
295 sl_fclose(FIL__, __LINE__, fd);
296 return;
297 }
298 }
299 sl_fclose(FIL__, __LINE__, fd);
300 }
301 }
302 SH_MUTEX_LOCK(mutex_thread_nolog);
303 sh_error_handle((-1), FIL__, __LINE__, 0, MSG_E_SUBGEN,
[371]304 _("Cannot set max_user_watches"),
305 _("sh_fInotify_set_nwatches"));
[368]306 SH_MUTEX_UNLOCK(mutex_thread_nolog);
[376]307 fails = 1;
[368]308 return;
309}
310
311/* The watch fd is thread specific. To have it in the fInotify thread,
312 * the main thread writes a list of files/dirs to watch, and here we
313 * now pop files from the list to add watches for them.
314 */
315static int sh_fInotify_init_internal()
316{
317 char * filename;
318 int class;
[372]319 int type;
[373]320 int rdepth;
[368]321 unsigned long check_mask;
322 int retval;
323 int errnum;
324
325 if (ShfInotifyActive == S_FALSE)
326 return SH_MOD_FAILED;
327
328 /* Wait until file scan is finished.
329 */
330 while((sh.flag.inotify & SH_INOTIFY_DOSCAN) != 0)
331 {
332 retry_msleep(1,0);
333
334 if (ShfInotifyActive == S_FALSE)
335 return SH_MOD_FAILED;
336 }
337
338 sh_fInotify_set_nwatches();
339
340 while (NULL != (filename = sh_inotify_pop_dormant(&sh_file_watches,
[373]341 &class, &check_mask,
342 &type, &rdepth)))
[368]343 {
344 retval = sh_inotify_add_watch(filename, &sh_file_watches, &errnum,
[373]345 class, check_mask, type, rdepth);
[368]346
347 if (retval < 0)
348 {
349 char errbuf[SH_ERRBUF_SIZE];
350
351 sh_error_message(errnum, errbuf, sizeof(errbuf));
352
353 if ((errnum == ENOENT) || (errnum == EEXIST))
354 {
355 char * epath = sh_util_safe_name (filename);
356 SH_MUTEX_LOCK(mutex_thread_nolog);
357 sh_error_handle( (class == SH_LEVEL_ALLIGNORE) ?
358 ShDFLevel[class] :
359 ShDFLevel[SH_ERR_T_FILE],
360 FIL__, __LINE__, errnum, MSG_E_SUBGPATH,
361 errbuf, _("sh_fInotify_init_internal"), epath);
362 SH_MUTEX_UNLOCK(mutex_thread_nolog);
363 SH_FREE(epath);
364 }
365 else
366 {
367 SH_MUTEX_LOCK(mutex_thread_nolog);
368 sh_error_handle((-1), FIL__, __LINE__, errnum, MSG_E_SUBGEN,
369 errbuf, _("sh_fInotify_init_internal"));
370 SH_MUTEX_UNLOCK(mutex_thread_nolog);
371 }
372 }
373 }
374
375 /* Need this because mod_check() may run after
376 * DOSCAN is finished, hence wouldn't call init().
377 */
378 sh.flag.inotify &= ~SH_INOTIFY_NEEDINIT;
379
380 return 0;
381}
382
[373]383static void sh_fInotify_logmask(struct inotify_event * event)
384{
385 char dbgbuf[256];
386
387 sl_strlcpy (dbgbuf, "inotify mask: ", sizeof(dbgbuf));
388
389 if (event->mask & IN_ACCESS) sl_strlcat(dbgbuf, "IN_ACCESS ", sizeof(dbgbuf));
390 if (event->mask & IN_ATTRIB) sl_strlcat(dbgbuf, "IN_ATTRIB ", sizeof(dbgbuf));
391 if (event->mask & IN_CLOSE_WRITE) sl_strlcat(dbgbuf, "IN_CLOSE_WRITE ", sizeof(dbgbuf));
392 if (event->mask & IN_CLOSE_NOWRITE) sl_strlcat(dbgbuf, "IN_CLOSE_NOWRITE ", sizeof(dbgbuf));
393 if (event->mask & IN_CREATE) sl_strlcat(dbgbuf, "IN_CREATE ", sizeof(dbgbuf));
394 if (event->mask & IN_DELETE) sl_strlcat(dbgbuf, "IN_DELETE ", sizeof(dbgbuf));
395 if (event->mask & IN_DELETE_SELF) sl_strlcat(dbgbuf, "IN_DELETE_SELF ", sizeof(dbgbuf));
396 if (event->mask & IN_MODIFY) sl_strlcat(dbgbuf, "IN_MODIFY ", sizeof(dbgbuf));
397 if (event->mask & IN_MOVE_SELF) sl_strlcat(dbgbuf, "IN_MOVE_SELF ", sizeof(dbgbuf));
398 if (event->mask & IN_MOVED_FROM) sl_strlcat(dbgbuf, "IN_MOVED_FROM ", sizeof(dbgbuf));
399 if (event->mask & IN_MOVED_TO) sl_strlcat(dbgbuf, "IN_MOVED_TO ", sizeof(dbgbuf));
400 if (event->mask & IN_OPEN) sl_strlcat(dbgbuf, "IN_OPEN ", sizeof(dbgbuf));
401 if (event->mask & IN_IGNORED) sl_strlcat(dbgbuf, "IN_IGNORED ", sizeof(dbgbuf));
402 if (event->mask & IN_ISDIR) sl_strlcat(dbgbuf, "IN_ISDIR ", sizeof(dbgbuf));
403 if (event->mask & IN_Q_OVERFLOW) sl_strlcat(dbgbuf, "IN_Q_OVERFLOW ", sizeof(dbgbuf));
404 if (event->mask & IN_UNMOUNT) sl_strlcat(dbgbuf, "IN_UNMOUNT ", sizeof(dbgbuf));
405
406 /* fprintf(stderr, "FIXME: %s\n", dbgbuf); */
407
408 SH_MUTEX_LOCK(mutex_thread_nolog);
409 sh_error_handle(SH_ERR_ALL, FIL__, __LINE__, 0, MSG_E_SUBGEN,
410 dbgbuf, _("sh_fInotify_process"));
411 SH_MUTEX_UNLOCK(mutex_thread_nolog);
412}
413
[368]414static int sh_fInotify_process(struct inotify_event * event)
415{
416 int class;
[372]417 int ftype;
[373]418 int rdepth;
[368]419 unsigned long check_mask;
420 char * filename;
[371]421 extern int flag_err_debug;
[368]422
[371]423 if (flag_err_debug == SL_TRUE)
424 {
[373]425 sh_fInotify_logmask(event);
[371]426 }
427
[368]428 if (event->wd >= 0)
429 {
430 filename = sh_inotify_search_item(&sh_file_watches, event->wd,
[373]431 &class, &check_mask, &ftype, &rdepth);
[368]432
433 if (filename)
434 {
[373]435 sh_fInotify_report(event, filename, class, check_mask, ftype, rdepth);
[368]436 SH_FREE(filename);
437 }
438 else if (sh.flag.inotify & SH_INOTIFY_NEEDINIT)
439 {
440 return 1;
441 }
442 else if ((event->mask & IN_UNMOUNT) == 0 && (event->mask & IN_IGNORED) == 0)
443 {
444 /* Remove watch ? Seems reasonable. */
445 sh_inotify_rm_watch(NULL, NULL, event->wd);
[372]446
[368]447 SH_MUTEX_LOCK(mutex_thread_nolog);
448 sh_error_handle((-1), FIL__, __LINE__, event->wd, MSG_E_SUBGEN,
[373]449 _("Watch removed: file path unknown"),
[368]450 _("sh_fInotify_process"));
451 SH_MUTEX_UNLOCK(mutex_thread_nolog);
452 }
453 }
454 else if ((event->mask & IN_Q_OVERFLOW) != 0)
455 {
456 sh.flag.inotify |= SH_INOTIFY_DOSCAN;
457 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
[373]458 SH_MUTEX_LOCK(mutex_thread_nolog);
459 sh_error_handle(SH_ERR_WARN, FIL__, __LINE__, event->wd, MSG_E_SUBGEN,
460 _("Inotify queue overflow"),
461 _("sh_fInotify_process"));
462 SH_MUTEX_UNLOCK(mutex_thread_nolog);
[368]463 return 1;
464 }
465
466 return 0;
467}
468
469void sh_fInotify_report_add(char * path, int class, unsigned long check_mask)
470{
471 if (S_FALSE == sh_ignore_chk_new(path))
472 {
473 int reported = 0;
474
475 sh_files_clear_file_reported(path);
476
477 sh_files_search_file(path, &class, &check_mask, &reported);
478
479 sh_files_filecheck (class, check_mask, path, NULL,
480 &reported, 0);
481 if (SH_FFLAG_REPORTED_SET(reported))
482 sh_files_set_file_reported(path);
483 }
484 return;
485}
486
487
488static void sh_fInotify_report_miss(char * name, int level)
489{
490 char * tmp = sh_util_safe_name (name);
491
492 SH_MUTEX_LOCK(mutex_thread_nolog);
493 sh_error_handle (level, FIL__, __LINE__, 0, MSG_FI_MISS, tmp);
494 SH_MUTEX_UNLOCK(mutex_thread_nolog);
495
496 SH_FREE(tmp);
497 return;
498}
499
[373]500static int sh_fInotify_report_change (struct inotify_event * event,
501 char * path, char * filename,
502 int class, unsigned long check_mask, int ftype)
[368]503{
504 int reported;
[373]505 int ret = sh_files_search_file(path, &class, &check_mask, &reported);
[368]506
[373]507 if ((ret == 0) && (event->len > 0) && (ftype == SH_INOTIFY_FILE))
[368]508 {
[373]509 ; /* do nothing, watch was for directory monitored as file only */
[368]510 }
511 else
512 {
[373]513 sh_files_filecheck (class, check_mask, filename,
514 (event->len > 0) ? event->name : NULL,
515 &reported, 0);
[368]516 }
[373]517 return 0;
518}
[368]519
520
[373]521static int sh_fInotify_report_missing (struct inotify_event * event,
522 char * path,
523 int class, unsigned long check_mask, int ftype)
524{
525 int reported;
526 int isdir = (event->mask & IN_ISDIR);
527 int level = (class == SH_LEVEL_ALLIGNORE) ?
528 ShDFLevel[class] :
529 ShDFLevel[(isdir == 0) ? SH_ERR_T_FILE : SH_ERR_T_DIR];
530
531 if (S_FALSE == sh_ignore_chk_del(path))
[368]532 {
[373]533 if (0 != hashreport_missing(path, level))
[368]534 {
[373]535 int ret = sh_files_search_file(path, &class, &check_mask, &reported);
536
537 if ((ret == 0) && (event->len > 0) && (ftype == SH_INOTIFY_FILE))
[368]538 {
[373]539 ; /* do nothing, watch was for directory monitored as file only */
[368]540 }
[373]541 else
542 {
543 /* Removal of a directory triggers:
544 * (1) IN_DELETE IN_ISDIR
545 * (2) IN_DELETE_SELF
546 */
547 if ((event->mask & IN_DELETE_SELF) == 0)
548 sh_fInotify_report_miss(path, level);
549 }
[368]550 }
[373]551 }
[368]552
553#ifndef REPLACE_OLD
[373]554 sh_hash_set_visited_true(path);
[368]555#else
[373]556 sh_hash_set_missing(path);
[368]557#endif
[373]558 if (sh.flag.reportonce == S_TRUE)
559 sh_files_set_file_reported(path);
[368]560
[373]561 /* Move to 'dormant' list, if not file within directory.
562 */
563 if (event->len == 0)
564 sh_inotify_rm_watch(&sh_file_watches, &sh_file_missing, event->wd);
565
566 return 0;
567}
568
569static int sh_fInotify_report_added (struct inotify_event * event,
570 char * path, char * filename,
571 int class, unsigned long check_mask,
572 int ftype, int rdepth)
573{
574 if (S_FALSE == sh_ignore_chk_new(path))
[368]575 {
[373]576 int reported;
577 int ret;
578 int retD = 0;
579 int rdepthD = rdepth;
580
581 sh_files_clear_file_reported(path);
582
583 ret = sh_files_search_file(path, &class, &check_mask, &reported);
584
585 if ((ret == 0) && (event->len > 0) && (ftype == SH_INOTIFY_FILE))
[368]586 {
[373]587 ; /* do nothing, watch was for directory monitored as file only */
588 }
589 else
590 {
591 int classD = class;
592 int reportedD = reported;
593 unsigned long check_maskD = check_mask;
[368]594
[373]595 if (event->mask & IN_ISDIR)
[372]596 {
[373]597 retD = sh_files_search_dir(path, &classD, &check_maskD,
598 &reportedD, &rdepthD);
599 if (retD != 0)
600 {
601 if (ret == 0)
602 {
603 class = classD;
604 check_mask = check_maskD;
605 }
606 }
[372]607 }
[373]608
609 sh_files_filecheck (class, check_mask, filename,
610 (event->len > 0) ? event->name : NULL,
611 &reported, 0);
612
613 if (event->mask & IN_ISDIR)
[372]614 {
[373]615 sh.flag.inotify |= SH_INOTIFY_INSCAN;
616 sh_files_checkdir (classD, check_maskD, rdepthD,
617 path, (event->len > 0) ? event->name : NULL);
618 sh.flag.inotify &= ~SH_INOTIFY_INSCAN;
619 sh.flag.inotify |= SH_INOTIFY_NEEDINIT;
620 sh_dirs_reset ();
621 sh_files_reset ();
[372]622 }
[373]623
624 }
625
626 if (SH_FFLAG_REPORTED_SET(reported))
627 sh_files_set_file_reported(path);
628
629 if ((ret != 0) || (event->mask & IN_ISDIR))
630 {
631 sh_inotify_add_watch(path, &sh_file_watches, &ret,
632 class, check_mask,
633 (event->mask & IN_ISDIR)?SH_INOTIFY_DIR:SH_INOTIFY_FILE,
634 rdepthD);
635 }
636 }
637 return 0;
638}
[372]639
[373]640static int sh_fInotify_report(struct inotify_event * event, char * filename,
641 int class, unsigned long check_mask, int ftype, int rdepth)
642{
643 char * fullpath = NULL;
644 char * path;
[371]645
[373]646 if (event->len > 0)
647 {
648 fullpath = sh_util_strconcat(filename, "/", event->name, NULL);
649 path = fullpath;
[368]650 }
[373]651 else
652 {
653 path = filename;
654 }
[368]655
[373]656 if ( (event->mask & (IN_ATTRIB|IN_MODIFY)) != 0)
657 {
658 sh_fInotify_report_change (event, path, filename,
659 class, check_mask, ftype);
660 }
661 else if ((event->mask & (IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF|IN_MOVED_FROM)) != 0)
662 {
663 sh_fInotify_report_missing (event, path,
664 class, check_mask, ftype);
665 }
666 else if((event->mask & (IN_CREATE|IN_MOVED_TO)) != 0)
667 {
668 sh_fInotify_report_added (event, path, filename,
669 class, check_mask,
670 ftype, rdepth);
671 }
672
673 if (fullpath)
674 SH_FREE(fullpath);
675
[368]676 return 0;
677}
678
679
680#endif
681
682#endif
Note: See TracBrowser for help on using the repository browser.