635 lines
15 KiB
C
635 lines
15 KiB
C
/// \file
|
|
/// \brief Support to find files
|
|
///
|
|
///
|
|
/// filesearch:
|
|
///
|
|
/// ATTENTION : make sure there is enouth space in filename to put a full path (255 or 512)
|
|
/// if wantedhash == 0 there is no hash check
|
|
/// if completepath then filename will be change with the full path and name
|
|
/// maxsearchdepth == 0 only search given directory, no subdirs
|
|
/// return FS_NOTFOUND
|
|
/// FS_BADHASH;
|
|
/// FS_FOUND
|
|
|
|
#include <stdio.h>
|
|
#ifdef __GNUC__
|
|
#include <dirent.h>
|
|
#endif
|
|
#if defined (_WIN32) && !defined (_XBOX)
|
|
//#define WIN32_LEAN_AND_MEAN
|
|
#define RPC_NO_WINDOWS_H
|
|
#include <windows.h>
|
|
#endif
|
|
#include <sys/stat.h>
|
|
#include <string.h>
|
|
|
|
#include "filesrch.h"
|
|
#include "d_netfil.h"
|
|
#include "m_misc.h"
|
|
#include "z_zone.h"
|
|
#include "m_menu.h" // Addons_option_Onchange
|
|
|
|
static CV_PossibleValue_t addons_cons_t[] = {{0, "Default"}, {1, "HOME"}, {2, "SRB2KART"}, {3, "CUSTOM"}, {0, NULL}};
|
|
consvar_t cv_addons_option = CVAR_INIT ("addons_option", "Default", CV_SAVE|CV_CALL|CV_NOINIT, addons_cons_t, Addons_option_Onchange);
|
|
consvar_t cv_addons_folder = CVAR_INIT ("addons_folder", "", CV_SAVE, NULL, NULL);
|
|
|
|
static CV_PossibleValue_t addons_md5_cons_t[] = {{0, "Name"}, {1, "Contents"}, {0, NULL}};
|
|
consvar_t cv_addons_md5 = CVAR_INIT ("addons_md5", "Name", CV_SAVE, addons_md5_cons_t, NULL);
|
|
|
|
consvar_t cv_addons_showall = CVAR_INIT ("addons_showall", "No", CV_SAVE, CV_YesNo, NULL);
|
|
|
|
consvar_t cv_addons_search_case = CVAR_INIT ("addons_search_case", "No", CV_SAVE, CV_YesNo, NULL);
|
|
|
|
static CV_PossibleValue_t addons_search_type_cons_t[] = {{0, "Start"}, {1, "Anywhere"}, {0, NULL}};
|
|
consvar_t cv_addons_search_type = CVAR_INIT ("addons_search_type", "Anywhere", CV_SAVE, addons_search_type_cons_t, NULL);
|
|
|
|
char menupath[MAXFILEPATH];
|
|
size_t menupathindex[menudepth];
|
|
size_t menudepthleft = menudepth;
|
|
|
|
char menusearchbuf[MAXSTRINGLENGTH+1];
|
|
textinput_t menusearch;
|
|
|
|
char **dirmenu, **coredirmenu; // core only local for this file
|
|
size_t sizedirmenu, sizecoredirmenu; // ditto
|
|
size_t dir_on[menudepth];
|
|
UINT8 refreshdirmenu = 0;
|
|
char *refreshdirname = NULL;
|
|
|
|
// skip those folders, they will not have any addons
|
|
static const char *exclude_paths[] = {
|
|
"logs",
|
|
"models",
|
|
"media",
|
|
NULL
|
|
};
|
|
|
|
filestatus_t filesearch(char *filename, const char *startpath, UINT64 wantedhash, boolean completepath, int maxsearchdepth)
|
|
{
|
|
filestatus_t retval = FS_NOTFOUND;
|
|
DIR **dirhandle;
|
|
struct dirent *dent;
|
|
#ifndef _WIN32
|
|
struct stat fsstat;
|
|
#endif
|
|
int found = 0;
|
|
char *searchname;
|
|
int depthleft = maxsearchdepth;
|
|
char searchpath[MAXFILEPATH];
|
|
size_t *searchpathindex;
|
|
|
|
dirhandle = (DIR**)malloc(maxsearchdepth * sizeof(DIR*));
|
|
searchpathindex = (size_t *)malloc(maxsearchdepth * sizeof(size_t));
|
|
|
|
strcpy(searchpath, startpath);
|
|
searchpathindex[--depthleft] = strlen(searchpath) + 1;
|
|
|
|
dirhandle[depthleft] = opendir(searchpath);
|
|
|
|
if (dirhandle[depthleft] == NULL)
|
|
{
|
|
free(dirhandle);
|
|
free(searchpathindex);
|
|
return FS_NOTFOUND;
|
|
}
|
|
|
|
searchname = strdup(filename);
|
|
|
|
if (searchpath[searchpathindex[depthleft]-2] != PATHSEP[0])
|
|
{
|
|
searchpath[searchpathindex[depthleft]-1] = PATHSEP[0];
|
|
searchpath[searchpathindex[depthleft]] = 0;
|
|
}
|
|
else
|
|
searchpathindex[depthleft]--;
|
|
|
|
while ((!found) && (depthleft < maxsearchdepth))
|
|
{
|
|
searchpath[searchpathindex[depthleft]]=0;
|
|
dent = readdir(dirhandle[depthleft]);
|
|
|
|
if (!dent)
|
|
{
|
|
closedir(dirhandle[depthleft++]);
|
|
continue;
|
|
}
|
|
|
|
if (dent->d_name[0]=='.' &&
|
|
(dent->d_name[1]=='\0' ||
|
|
(dent->d_name[1]=='.' &&
|
|
dent->d_name[2]=='\0')))
|
|
{
|
|
// we don't want to scan uptree
|
|
continue;
|
|
}
|
|
|
|
// okay, now we actually want searchpath to incorporate d_name
|
|
strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
|
|
|
|
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__)
|
|
if (dent->d_type == DT_UNKNOWN || dent->d_type == DT_LNK)
|
|
if (stat(searchpath, &fsstat) == 0 && S_ISDIR(fsstat.st_mode))
|
|
dent->d_type = DT_DIR;
|
|
|
|
// Linux and FreeBSD has a special field for file type on dirent, so use that to speed up lookups.
|
|
if (dent->d_type == DT_DIR)
|
|
#elif defined (_WIN32)
|
|
// if we wanna follow symlinks we can check with FILE_ATTRIBUTE_REPARSE_POINT
|
|
DWORD fileattr = GetFileAttributes(searchpath);
|
|
if (fileattr == INVALID_FILE_ATTRIBUTES)
|
|
continue; // was the file (re)moved? can't stat it
|
|
|
|
if (fileattr & FILE_ATTRIBUTE_DIRECTORY)
|
|
#else
|
|
if (stat(searchpath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
|
|
continue; // was the file (re)moved? can't stat it
|
|
|
|
if (S_ISDIR(fsstat.st_mode))
|
|
#endif
|
|
{
|
|
// I am a folder!
|
|
|
|
if (!depthleft)
|
|
continue; // No additional folder delving permitted...
|
|
|
|
const char **path = exclude_paths;
|
|
|
|
if (depthleft == maxsearchdepth-1)
|
|
{
|
|
// When we're at the root of the search, we exclude certain folders.
|
|
|
|
boolean skipfolder = false;
|
|
|
|
for (; *path != NULL; path++)
|
|
{
|
|
if (strcasecmp(*path, dent->d_name) == 0)
|
|
{
|
|
skipfolder = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This folder is excluded
|
|
if (skipfolder)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (strcasecmp(".git", dent->d_name) // sanity if you're weird like me
|
|
&& (dirhandle[depthleft-1] = opendir(searchpath)) != NULL)
|
|
{
|
|
// Got read permissions!
|
|
searchpathindex[--depthleft] = strlen(searchpath) + 1;
|
|
|
|
searchpath[searchpathindex[depthleft]-1] = PATHSEP[0];
|
|
searchpath[searchpathindex[depthleft]] = 0;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// I am a file!
|
|
|
|
if (strcasecmp(searchname, dent->d_name))
|
|
continue; // Not what we're looking for!
|
|
|
|
switch (checkfilehash(searchpath, wantedhash))
|
|
{
|
|
case FS_FOUND:
|
|
if (completepath)
|
|
strcpy(filename, searchpath);
|
|
else
|
|
strcpy(filename, dent->d_name);
|
|
retval = FS_FOUND;
|
|
found = 1;
|
|
break;
|
|
case FS_BADHASH:
|
|
retval = FS_BADHASH;
|
|
break;
|
|
default: // prevent some compiler warnings
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (; depthleft < maxsearchdepth; closedir(dirhandle[depthleft++]));
|
|
|
|
free(searchname);
|
|
free(searchpathindex);
|
|
free(dirhandle);
|
|
|
|
return retval;
|
|
}
|
|
|
|
char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) plus 3 (null terminator, stop, and length including previous two)
|
|
"\5.txt", "\5.cfg", // exec
|
|
"\5.wad",
|
|
#ifdef USE_KART
|
|
"\6.kart",
|
|
#endif
|
|
"\5.pk3", "\5.soc", "\5.lua"}; // addfile
|
|
|
|
char filenamebuf[MAX_WADFILES][MAX_WADPATH];
|
|
|
|
|
|
static boolean filemenucmp(char *haystack, char *needle)
|
|
{
|
|
static char localhaystack[128];
|
|
strlcpy(localhaystack, haystack, 128);
|
|
if (!cv_addons_search_case.value)
|
|
strupr(localhaystack);
|
|
if (cv_addons_search_type.value)
|
|
return (strstr(localhaystack, needle) != 0);
|
|
return (!strncmp(localhaystack, needle, menusearch.length));
|
|
}
|
|
|
|
void closefilemenu(boolean validsize)
|
|
{
|
|
// search
|
|
if (dirmenu)
|
|
{
|
|
if (dirmenu != coredirmenu)
|
|
{
|
|
if (dirmenu[0] && ((UINT8)(dirmenu[0][DIR_TYPE]) == EXT_NORESULTS))
|
|
{
|
|
Z_Free(dirmenu[0]);
|
|
dirmenu[0] = NULL;
|
|
}
|
|
Z_Free(dirmenu);
|
|
}
|
|
dirmenu = NULL;
|
|
sizedirmenu = 0;
|
|
}
|
|
|
|
if (coredirmenu)
|
|
{
|
|
// core
|
|
if (validsize)
|
|
{
|
|
for (; sizecoredirmenu > 0; sizecoredirmenu--)
|
|
{
|
|
Z_Free(coredirmenu[sizecoredirmenu-1]);
|
|
coredirmenu[sizecoredirmenu-1] = NULL;
|
|
}
|
|
}
|
|
else
|
|
sizecoredirmenu = 0;
|
|
|
|
Z_Free(coredirmenu);
|
|
coredirmenu = NULL;
|
|
}
|
|
|
|
if (refreshdirname)
|
|
Z_Free(refreshdirname);
|
|
refreshdirname = NULL;
|
|
}
|
|
|
|
void searchfilemenu(char *tempname)
|
|
{
|
|
size_t i, first;
|
|
char localmenusearch[MAXSTRINGLENGTH] = "";
|
|
|
|
if (dirmenu)
|
|
{
|
|
if (dirmenu != coredirmenu)
|
|
{
|
|
if (dirmenu[0] && ((UINT8)(dirmenu[0][DIR_TYPE]) == EXT_NORESULTS))
|
|
{
|
|
Z_Free(dirmenu[0]);
|
|
dirmenu[0] = NULL;
|
|
}
|
|
//Z_Free(dirmenu); -- Z_Realloc later tho...
|
|
}
|
|
else
|
|
dirmenu = NULL;
|
|
}
|
|
|
|
first = (((UINT8)(coredirmenu[0][DIR_TYPE]) == EXT_UP) ? 1 : 0); // skip UP...
|
|
|
|
if (!menusearch.length)
|
|
{
|
|
if (dirmenu)
|
|
Z_Free(dirmenu);
|
|
dirmenu = coredirmenu;
|
|
sizedirmenu = sizecoredirmenu;
|
|
|
|
if (tempname)
|
|
{
|
|
for (i = first; i < sizedirmenu; i++)
|
|
{
|
|
if (fastcmp(dirmenu[i]+DIR_STRING, tempname))
|
|
{
|
|
dir_on[menudepthleft] = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i == sizedirmenu)
|
|
dir_on[menudepthleft] = first;
|
|
|
|
Z_Free(tempname);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
strcpy(localmenusearch, menusearch.buffer);
|
|
if (!cv_addons_search_case.value)
|
|
strupr(localmenusearch);
|
|
|
|
sizedirmenu = 0;
|
|
for (i = first; i < sizecoredirmenu; i++)
|
|
{
|
|
if (filemenucmp(coredirmenu[i]+DIR_STRING, localmenusearch))
|
|
sizedirmenu++;
|
|
}
|
|
|
|
if (!sizedirmenu) // no results...
|
|
{
|
|
if ((!(dirmenu = Z_Realloc(dirmenu, sizeof(char *), PU_STATIC, NULL)))
|
|
|| !(dirmenu[0] = Z_StrDup(va("%c\13No results...", EXT_NORESULTS))))
|
|
I_Error("searchfilemenu(): could not create \"No results...\".");
|
|
sizedirmenu = 1;
|
|
dir_on[menudepthleft] = 0;
|
|
if (tempname)
|
|
Z_Free(tempname);
|
|
return;
|
|
}
|
|
|
|
if (!(dirmenu = Z_Realloc(dirmenu, sizedirmenu*sizeof(char *), PU_STATIC, NULL)))
|
|
I_Error("searchfilemenu(): could not reallocate dirmenu.");
|
|
|
|
sizedirmenu = 0;
|
|
for (i = first; i < sizecoredirmenu; i++)
|
|
{
|
|
if (filemenucmp(coredirmenu[i]+DIR_STRING, localmenusearch))
|
|
{
|
|
if (tempname && fastcmp(coredirmenu[i]+DIR_STRING, tempname))
|
|
{
|
|
dir_on[menudepthleft] = sizedirmenu;
|
|
Z_Free(tempname);
|
|
tempname = NULL;
|
|
}
|
|
dirmenu[sizedirmenu++] = coredirmenu[i]; // pointer reuse
|
|
}
|
|
}
|
|
|
|
if (tempname)
|
|
{
|
|
dir_on[menudepthleft] = 0; //first; -- can't be first, causes problems
|
|
Z_Free(tempname);
|
|
}
|
|
}
|
|
|
|
boolean preparefilemenu(boolean samedepth, boolean replayhut)
|
|
{
|
|
DIR *dirhandle;
|
|
struct dirent *dent;
|
|
#ifndef _WIN32
|
|
struct stat fsstat;
|
|
#endif
|
|
size_t pos = 0, folderpos = 0, numfolders = 0;
|
|
char *tempname = NULL;
|
|
|
|
if (samedepth)
|
|
{
|
|
if (dirmenu && dirmenu[dir_on[menudepthleft]])
|
|
tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL
|
|
}
|
|
else
|
|
M_TextInputInit(&menusearch, menusearchbuf, MAXSTRINGLENGTH+1);
|
|
|
|
if (!(dirhandle = opendir(menupath))) // get directory
|
|
{
|
|
closefilemenu(true);
|
|
return false;
|
|
}
|
|
|
|
if (dirmenu != coredirmenu)
|
|
Z_Free(dirmenu);
|
|
dirmenu = NULL;
|
|
|
|
if (coredirmenu != NULL)
|
|
{
|
|
for (; sizecoredirmenu > 0; sizecoredirmenu--) // clear out existing items
|
|
{
|
|
Z_Free(coredirmenu[sizecoredirmenu-1]);
|
|
coredirmenu[sizecoredirmenu-1] = NULL;
|
|
}
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
menupath[menupathindex[menudepthleft]] = 0;
|
|
dent = readdir(dirhandle);
|
|
|
|
if (!dent)
|
|
break;
|
|
else if (dent->d_name[0]=='.' &&
|
|
(dent->d_name[1]=='\0' ||
|
|
(dent->d_name[1]=='.' &&
|
|
dent->d_name[2]=='\0')))
|
|
continue; // we don't want to scan uptree
|
|
|
|
strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
|
|
|
|
#ifndef _WIN32
|
|
if (stat(menupath, &fsstat) < 0)
|
|
#else
|
|
// if we wanna follow symlinks we can check with FILE_ATTRIBUTE_REPARSE_POINT
|
|
DWORD fileattr = GetFileAttributes(menupath);
|
|
if (fileattr == INVALID_FILE_ATTRIBUTES)
|
|
#endif
|
|
; // was the file (re)moved? can't stat it
|
|
else // is a file or directory
|
|
{
|
|
#ifndef _WIN32
|
|
if (!S_ISDIR(fsstat.st_mode)) // file
|
|
#else
|
|
if (!(fileattr & FILE_ATTRIBUTE_DIRECTORY))
|
|
#endif
|
|
{
|
|
size_t len = strlen(dent->d_name)+1;
|
|
if (replayhut)
|
|
{
|
|
if (strcasecmp(".lmp", dent->d_name+len-5))
|
|
continue; // Not a replay
|
|
}
|
|
else if (!cv_addons_showall.value)
|
|
{
|
|
UINT8 ext;
|
|
for (ext = 0; ext < NUM_EXT_TABLE; ext++)
|
|
if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0])))
|
|
break; // extension comparison
|
|
if (ext == NUM_EXT_TABLE)
|
|
continue; // not an addfile-able (or exec-able) file
|
|
}
|
|
}
|
|
else // directory
|
|
numfolders++;
|
|
|
|
sizecoredirmenu++;
|
|
}
|
|
}
|
|
|
|
if (!sizecoredirmenu)
|
|
{
|
|
closedir(dirhandle);
|
|
closefilemenu(false);
|
|
if (tempname)
|
|
Z_Free(tempname);
|
|
return false;
|
|
}
|
|
|
|
if (menudepthleft != menudepth-1) // Make room for UP...
|
|
{
|
|
sizecoredirmenu++;
|
|
numfolders++;
|
|
folderpos++;
|
|
}
|
|
|
|
if (dirmenu && dirmenu == coredirmenu)
|
|
dirmenu = NULL;
|
|
|
|
if (!(coredirmenu = Z_Realloc(coredirmenu, sizecoredirmenu*sizeof(char *), PU_STATIC, NULL)))
|
|
{
|
|
closedir(dirhandle); // just in case
|
|
I_Error("preparefilemenu(): could not reallocate coredirmenu.");
|
|
}
|
|
|
|
rewinddir(dirhandle);
|
|
|
|
while ((pos+folderpos) < sizecoredirmenu)
|
|
{
|
|
menupath[menupathindex[menudepthleft]] = 0;
|
|
dent = readdir(dirhandle);
|
|
|
|
if (!dent)
|
|
break;
|
|
else if (dent->d_name[0]=='.' &&
|
|
(dent->d_name[1]=='\0' ||
|
|
(dent->d_name[1]=='.' &&
|
|
dent->d_name[2]=='\0')))
|
|
continue; // we don't want to scan uptree
|
|
|
|
strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
|
|
|
|
#ifndef _WIN32
|
|
if (stat(menupath, &fsstat) < 0)
|
|
#else
|
|
// if we wanna follow symlinks we can check with FILE_ATTRIBUTE_REPARSE_POINT
|
|
DWORD fileattr = GetFileAttributes(menupath);
|
|
if (fileattr == INVALID_FILE_ATTRIBUTES)
|
|
#endif
|
|
; // was the file (re)moved? can't stat it
|
|
else // is a file or directory
|
|
{
|
|
char *temp;
|
|
size_t len = strlen(dent->d_name)+1;
|
|
UINT8 ext = EXT_FOLDER;
|
|
UINT8 folder;
|
|
|
|
#ifndef _WIN32
|
|
if (!S_ISDIR(fsstat.st_mode)) // file
|
|
#else
|
|
if (!(fileattr & FILE_ATTRIBUTE_DIRECTORY))
|
|
#endif
|
|
{
|
|
if (!((numfolders+pos) < sizecoredirmenu))
|
|
continue; // crash prevention
|
|
|
|
if (replayhut)
|
|
{
|
|
if (strcasecmp(".lmp", dent->d_name+len-5))
|
|
continue; // Not a replay
|
|
ext = EXT_TXT; // This isn't used anywhere but better safe than sorry for messing with this...
|
|
}
|
|
else
|
|
{
|
|
for (; ext < NUM_EXT_TABLE; ext++)
|
|
if (!strcasecmp(exttable[ext]+1, dent->d_name+len-(exttable[ext][0])))
|
|
break; // extension comparison
|
|
if (ext == NUM_EXT_TABLE && !cv_addons_showall.value)
|
|
continue; // not an addfile-able (or exec-able) file
|
|
ext += EXT_START; // moving to be appropriate position
|
|
|
|
if (ext >= EXT_LOADSTART)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < numwadfiles; i++)
|
|
{
|
|
if (!filenamebuf[i][0])
|
|
{
|
|
strncpy(filenamebuf[i], wadfiles[i]->filename, MAX_WADPATH);
|
|
filenamebuf[i][MAX_WADPATH - 1] = '\0';
|
|
nameonly(filenamebuf[i]);
|
|
}
|
|
|
|
if (!fastcmp(dent->d_name, filenamebuf[i]))
|
|
continue;
|
|
|
|
if (cv_addons_md5.value && !checkfilehash(menupath, wadfiles[i]->hash))
|
|
continue;
|
|
|
|
ext |= EXT_LOADED;
|
|
}
|
|
}
|
|
else if (ext == EXT_TXT)
|
|
{
|
|
if (fastcmp(dent->d_name, "log.txt") || fastcmp(dent->d_name, "errorlog.txt"))
|
|
ext |= EXT_LOADED;
|
|
}
|
|
|
|
if (fastcmp(dent->d_name, configfile))
|
|
ext |= EXT_LOADED;
|
|
}
|
|
|
|
folder = 0;
|
|
}
|
|
else // directory
|
|
len += (folder = 1);
|
|
|
|
if (len > 255)
|
|
len = 255;
|
|
|
|
if (!(temp = Z_Malloc((len+DIR_STRING+folder) * sizeof (char), PU_STATIC, NULL)))
|
|
I_Error("preparefilemenu(): could not create file entry.");
|
|
|
|
temp[DIR_TYPE] = ext;
|
|
temp[DIR_LEN] = (UINT8)(len);
|
|
strlcpy(temp+DIR_STRING, dent->d_name, len);
|
|
|
|
if (folder)
|
|
{
|
|
strcpy(temp+len, PATHSEP);
|
|
coredirmenu[folderpos++] = temp;
|
|
}
|
|
else if (replayhut) // Reverse-alphabetical on just the files; acts as a fake "most recent first" with the current filename format
|
|
coredirmenu[sizecoredirmenu - 1 - pos++] = temp;
|
|
else
|
|
coredirmenu[numfolders + pos++] = temp;
|
|
}
|
|
}
|
|
|
|
closedir(dirhandle);
|
|
|
|
if ((menudepthleft != menudepth-1) // now for UP... entry
|
|
&& !(coredirmenu[0] = Z_StrDup(va("%c\5UP...", EXT_UP))))
|
|
I_Error("preparefilemenu(): could not create \"UP...\".");
|
|
|
|
menupath[menupathindex[menudepthleft]] = 0;
|
|
sizecoredirmenu = (numfolders+pos); // just in case things shrink between opening and rewind
|
|
|
|
if (!sizecoredirmenu)
|
|
{
|
|
dir_on[menudepthleft] = 0;
|
|
closefilemenu(false);
|
|
return false;
|
|
}
|
|
|
|
searchfilemenu(tempname);
|
|
|
|
return true;
|
|
}
|
|
|