summaryrefslogtreecommitdiff
path: root/app/bin/partcatalog.c
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff-webhosting.net>2020-08-22 14:05:41 +0200
committerJörg Frings-Fürst <debian@jff-webhosting.net>2020-08-22 14:05:41 +0200
commitb55285a77da0e0b829e4ce8d7e09debaabc68e15 (patch)
treef622559ef65bbdd3e1c5bdb06098a8f89eec0563 /app/bin/partcatalog.c
parentd3897ce090dbeb220ed2c782f095597e417cf3cc (diff)
parentd1ae75703e1ed81d65ea16946dcdb77e7a13adc9 (diff)
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'app/bin/partcatalog.c')
-rw-r--r--app/bin/partcatalog.c846
1 files changed, 846 insertions, 0 deletions
diff --git a/app/bin/partcatalog.c b/app/bin/partcatalog.c
new file mode 100644
index 0000000..a1db09c
--- /dev/null
+++ b/app/bin/partcatalog.c
@@ -0,0 +1,846 @@
+/** \file partcatalog.c
+* Manage the catalog of track parameter files
+*/
+/* XTrkCad - Model Railroad CAD
+* Copyright (C) 2019 Martin Fischer
+*
+* 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
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+*/
+
+#include <assert.h>
+#include <ctype.h>
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#include <search.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef WINDOWS
+ #include "include/dirent.h"
+#else
+ #include <dirent.h>
+#endif
+#include "fileio.h"
+#include "misc.h"
+#include "include/paramfile.h"
+#include "include/partcatalog.h"
+#include "paths.h"
+#include "include/stringxtc.h"
+#include "include/utf8convert.h"
+
+#if _MSC_VER > 1300
+ #define strnicmp _strnicmp
+ #define strdup _strdup
+#endif
+
+#define PUNCTUATION "+-*/.,&%=#"
+
+
+
+/**
+ * Create and initialize the linked list for the catalog entries
+ *
+ * \return pointer to first element
+ */
+
+CatalogEntry *
+InitCatalog(void)
+{
+ CatalogEntry *head;
+ CatalogEntry *tail;
+
+ /* allocate two pseudo nodes for beginning and end of list */
+ head = (CatalogEntry *)malloc(sizeof(CatalogEntry));
+ tail = (CatalogEntry *)malloc(sizeof(CatalogEntry));
+
+ head->next = tail;
+ tail->next = tail;
+
+
+
+ return (head);
+}
+
+/**
+ * Create a new CatalogEntry and add it to the linked list. The newly
+ * created entry is inserted into the list after the given position
+ *
+ * \param entry IN insertion point
+ * \return pointer to new entry
+ */
+
+static CatalogEntry *
+InsertIntoCatalogAfter(CatalogEntry *entry)
+{
+ CatalogEntry *newEntry = (CatalogEntry *)malloc(sizeof(CatalogEntry));
+ newEntry->next = entry->next;
+ entry->next = newEntry;
+ newEntry->files = 0;
+ newEntry->contents = NULL;
+
+ return (newEntry);
+}
+
+/**
+ * Count the elements in the linked list ignoring dummy elements
+ *
+ * \param listHeader IN the linked list
+ * \return the numberof elements
+ */
+
+unsigned
+CountCatalogEntries(CatalogEntry *listHeader)
+{
+ CatalogEntry *currentEntry = listHeader->next;
+ unsigned count = 0;
+
+ while (currentEntry != currentEntry->next) {
+ count++;
+ currentEntry = currentEntry->next;
+ }
+ return (count);
+}
+
+/**
+ * Empty a catalog. All data nodes including their allocated memory are freed. On
+ *
+ * \param listHeader IN the list
+ */
+
+void
+EmptyCatalog(CatalogEntry *listHeader)
+{
+ CatalogEntry *current = listHeader;
+
+ while (current->next != current->next->next) {
+ CatalogEntry *removedElement;
+ removedElement = current->next;
+ current->next = current->next->next;
+ if (removedElement->contents) {
+ free(removedElement->contents);
+ }
+ for (unsigned int i = 0; i < removedElement->files; i++) {
+ free(removedElement->fullFileName[i]);
+ }
+ free(removedElement);
+ }
+}
+
+/**
+ * Find the position in the list and add
+ *
+ * \param listHeader IN start of list
+ * \param contents IN contents to include
+ *
+ * \return CatalogEntry if found, NULL otherwise
+ */
+
+static CatalogEntry *
+InsertInOrder(CatalogEntry *listHeader, const char *contents)
+{
+ CatalogEntry *currentEntry = listHeader;
+
+ while (currentEntry->next != currentEntry->next->next) {
+ CatalogEntry *nextEntry = currentEntry->next;
+ if (XtcStricmp(nextEntry->contents, contents)>0) {
+ return InsertIntoCatalogAfter(currentEntry);
+ }
+ currentEntry = nextEntry;
+ }
+ return InsertIntoCatalogAfter(currentEntry);
+}
+/**
+ * Get the existing list element for a content
+ *
+ * \param listHeader IN start of list
+ * \param contents IN contents to search
+ * \param Do we log error messages or not
+ *
+ * \return CatalogEntry if found, NULL otherwise
+ */
+
+static CatalogEntry *
+IsExistingContents(CatalogEntry *listHeader, const char *contents, BOOL_T silent)
+{
+ CatalogEntry *currentEntry = listHeader->next;
+
+ while (currentEntry != currentEntry->next) {
+ if (!XtcStricmp(currentEntry->contents, contents)) {
+ if (!silent)
+ printf("%s already exists in %s\n", contents, currentEntry->fullFileName[0]);
+ return (currentEntry);
+ }
+ currentEntry = currentEntry->next;
+ }
+ return (NULL);
+}
+
+/**
+ * Store information about a parameter file. The filename is added to the array
+ * of files with identical content. If not already present, in addition
+ * the content is stored
+ *
+ * \param entry existing entry to be updated
+ * \param path filename to add
+ * \param contents contents description
+ */
+
+static void
+UpdateCatalogEntry(CatalogEntry *entry, char *path, char *contents)
+{
+ if (!entry->contents) {
+ entry->contents = strdup(contents);
+ }
+
+ if (entry->files < MAXFILESPERCONTENT) {
+ entry->fullFileName[entry->files++] = strdup(path);
+ } else {
+ AbortProg("Number of file with same content too large!", NULL);
+ }
+}
+
+/**
+ * Create the list for the catalog entries
+ *
+ * \return
+ */
+
+static CatalogEntry *
+CreateCatalog()
+{
+ CatalogEntry *catalog = InitCatalog();
+
+ return (catalog);
+}
+
+
+static IndexEntry *
+CreateIndexTable(unsigned int capacity)
+{
+ IndexEntry *index = (IndexEntry *)malloc(capacity * sizeof(IndexEntry));
+
+ return (index);
+}
+
+/**
+ * Scan opened directory for the next parameter file
+ *
+ * \param dir IN opened directory handle
+ * \param dirName IN name of directory
+ * \param fileName OUT fully qualified filename
+ *
+ * \return TRUE if file found, FALSE if not
+ */
+
+static bool
+GetNextParameterFile(DIR *dir, const char *dirName, char **fileName)
+{
+ bool done = false;
+ bool res = false;
+
+ /*
+ * get all files from the directory
+ */
+ while (!done) {
+ struct stat fileState;
+ struct dirent *ent;
+
+ ent = readdir(dir);
+
+ if (ent) {
+ if (!XtcStricmp(FindFileExtension(ent->d_name), "xtp")) {
+ /* create full file name and get the state for that file */
+ MakeFullpath(fileName, dirName, ent->d_name, NULL);
+
+ if (stat(*fileName, &fileState) == -1) {
+ fprintf(stderr, "Error getting file state for %s\n", *fileName);
+ continue;
+ }
+
+ /* ignore any directories */
+ if (!(fileState.st_mode & S_IFDIR)) {
+ done = true;
+ res = true;
+ }
+ }
+ } else {
+ done = true;
+ res = false;
+ }
+ }
+ return (res);
+}
+
+/**
+ * Scan a directory for parameter files. For each file found the CONTENTS is
+ * read and added to the list *
+ *
+ * \param insertAfter IN starting point for the list of files
+ * \param dirName IN directory to be scanned
+ *
+ * \return pointer to the last element(?)
+ */
+
+static CatalogEntry *
+ScanDirectory(CatalogEntry *catalog, const char *dirName)
+{
+ DIR *d;
+ CatalogEntry *newEntry = catalog;
+
+ d = opendir(dirName);
+ if (d) {
+ char *fileName = NULL;
+
+ while (GetNextParameterFile(d, dirName, &fileName)) {
+ CatalogEntry *existingEntry;
+ char *contents = GetParameterFileContent(fileName);
+ if ((existingEntry = IsExistingContents(catalog, contents,FALSE))) {
+ printf("Duplicate CONTENTS record in parameter file %s\n", fileName);
+ if (strcmp(existingEntry->fullFileName[existingEntry->files-1],fileName))
+ UpdateCatalogEntry(existingEntry, fileName, contents);
+ } else {
+ newEntry = InsertInOrder(catalog,contents);
+ UpdateCatalogEntry(newEntry, fileName, contents);
+ }
+ free(contents);
+ free(fileName);
+ fileName = NULL;
+ }
+ closedir(d);
+ }
+
+ return (newEntry);
+}
+
+/**
+ * Comparison function for IndexEntries used by qsort()
+ *
+ * \param entry1 IN
+ * \param entry2 IN
+ * \return per C runtime conventions
+ */
+
+static int
+CompareIndex(const void *entry1, const void *entry2)
+{
+ IndexEntry index1 = *(IndexEntry *)entry1;
+ IndexEntry index2 = *(IndexEntry *)entry2;
+ return (strcoll(index1.keyWord, index2.keyWord));
+}
+
+/*!
+ * Filter keywords. Current rules:
+ * - single character string that only consist of a punctuation char
+ *
+ * \param word IN keyword
+ * \return true if any rule applies, false otherwise
+ */
+
+bool
+FilterKeyword(char *word)
+{
+ if (strlen(word) == 1 && strpbrk(word, PUNCTUATION )) {
+ return(true);
+ }
+ return(false);
+}
+
+/**
+ * Create the keyword index from a list of parameter files
+ *
+ * \param catalog IN list of parameter files
+ * \param index IN index table to be filled
+ * \param pointer IN/OUT array of words that are indexed
+ * \param capacityOfIndex IN total maximum of keywords
+ * \return number of indexed keywords
+ */
+static unsigned
+CreateContentsIndex(CatalogEntry *catalog, IndexEntry *index, void** words_array,
+ unsigned capacityOfIndex)
+{
+ CatalogEntry *currentEntry = catalog->next;
+ unsigned totalMemory = 0;
+ size_t wordCount = 0;
+ char *wordList;
+ char *wordListPtr;
+
+ while (currentEntry != currentEntry->next) {
+ totalMemory += strlen(currentEntry->contents) + 1;
+ currentEntry = currentEntry->next;
+ }
+
+ wordList = malloc((totalMemory + 1) * sizeof(char));
+ *words_array = (void*)wordList;
+
+ wordListPtr = wordList;
+ currentEntry = catalog->next;
+
+ while (currentEntry != currentEntry->next) {
+ char *word;
+ char *content = strdup(currentEntry->contents);
+
+ word = strtok(content, " \t\n\r");
+ while (word && wordCount < capacityOfIndex) {
+ strcpy(wordListPtr, word);
+
+ char *p = wordListPtr;
+ for (; *p; ++p) {
+ *p = tolower(*p);
+ }
+ if (!FilterKeyword(wordListPtr)) {
+ index[wordCount].value = currentEntry;
+ index[wordCount].keyWord = wordListPtr;
+ wordListPtr += strlen(word) + 1;
+ wordCount++;
+ if (wordCount >= capacityOfIndex) {
+ AbortProg("Too many keywords were used!", NULL);
+ }
+ }
+ word = strtok(NULL, " \t\n\r");
+ }
+ free(content);
+ currentEntry = currentEntry->next;
+ }
+ *wordListPtr = '\0';
+
+ qsort((void*)index, wordCount, sizeof(IndexEntry), CompareIndex);
+
+ return (wordCount);
+}
+
+/**
+* A recursive binary search function. It returns location of x in
+* given array arr[l..r] is present, otherwise -1
+* Taken from http://www.geeksforgeeks.org/binary-search/ and modified
+*
+* \param arr IN array to search
+* \param l IN starting index
+* \param r IN highest index in array
+* \param key IN key to search
+* \return index if found, -1 otherwise
+*/
+
+static int SearchInIndex(IndexEntry arr[], int l, int r, char *key)
+{
+ if (r >= l) {
+ int mid = l + (r - l) / 2;
+ int res = XtcStricmp(key, arr[mid].keyWord);
+
+ // If the element is present at the middle itself
+ if (!res) {
+ return mid;
+ }
+
+ // If the array size is 1
+ if (r == 0) {
+ return -1;
+ }
+
+ // If element is smaller than mid, then it can only be present
+ // in left subarray
+ if (res < 0) {
+ return SearchInIndex(arr, l, mid - 1, key);
+ }
+
+ // Else the element can only be present in right subarray
+ return SearchInIndex(arr, mid + 1, r, key);
+ }
+
+ // We reach here when element is not present in array
+ return -1;
+}
+
+/**
+ * Inserts a key in arr[] of given capacity. n is current
+ * size of arr[]. This function returns n+1 if insertion
+ * is successful, else n.
+ * Taken from http ://www.geeksforgeeks.org/search-insert-and-delete-in-a-sorted-array/ and modified
+ */
+
+int InsertSorted(CatalogEntry *arr[], int n, CatalogEntry *key, int capacity)
+{
+ // Cannot insert more elements if n is already
+ // more than or equal to capcity
+ if (n >= capacity) {
+ return n;
+ }
+
+ int i;
+ for (i = n - 1; (i >= 0 && arr[i] > key); i--) {
+ arr[i + 1] = arr[i];
+ }
+
+ arr[i + 1] = key;
+
+ return (n + 1);
+}
+
+/**
+ * Comparison function for CatalogEntries used by qsort()
+ *
+ * \param entry1 IN
+ * \param entry2 IN
+ * \return per C runtime conventions
+ */
+
+static int
+CompareResults(const void *entry1, const void *entry2)
+{
+ CatalogEntry * index1 = *(CatalogEntry **)entry1;
+ CatalogEntry * index2 = *(CatalogEntry **)entry2;
+ return (strcoll(index1->contents, index2->contents));
+}
+
+/**
+ * Search the index for a keyword. The index is assumed to be sorted. So after one entry
+ * is found, neighboring entries up and down are checked as well. The total result set
+ * is placed into an array and returned. This array has to be free'd by the caller.
+ *
+ * \param index IN index list
+ * \param length IN number of entries index
+ * \param search IN search string
+ * \param resultCount OUT count of found entries
+ * \return array of found catalog entries, NULL if none found
+ */
+
+static int findAll = 1;
+
+unsigned int
+FindWord(IndexEntry *index, int length, char *search, CatalogEntry ***entries)
+{
+ CatalogEntry **result; //Array of pointers to Catalog Entries
+ int found;
+ int foundElements = 0;
+ *entries = NULL;
+
+ //Get all the entries back for generic search or if "generic find"
+ if (findAll || !search || (search[0] == '*') || (search[0] == '\0')) {
+ result = malloc((length) * sizeof(CatalogEntry *));
+ for (int i = 0; i < length; i++) {
+ result[i] = index[i].value;
+ }
+ *entries = result;
+ return length;
+ }
+
+ found = SearchInIndex(index, 0, length, search);
+
+ if (found >= 0) {
+ int lower = found;
+ int upper = found;
+ int i;
+
+ while (lower > 0 && !XtcStricmp(index[lower-1].keyWord, search)) {
+ lower--;
+ }
+
+ while (upper < length - 1 && !XtcStricmp(index[upper + 1].keyWord, search)) {
+ upper++;
+ }
+
+ foundElements = 1 + upper - lower;
+
+ result = malloc((foundElements) * sizeof(CatalogEntry *));
+
+ for (i = 0; i < foundElements; i++) {
+ result[i] = index[i+lower].value;
+ }
+
+ qsort((void*)result, foundElements, sizeof(void *), CompareResults);
+
+ *entries = result;
+ }
+ return (foundElements);
+}
+
+/**
+ * Create and initialize the data structure for the track library
+ *
+ * \param trackLibrary OUT the newly allocated track library
+ * \return TRUE on success
+ */
+
+TrackLibrary *
+InitLibrary(void)
+{
+ TrackLibrary *trackLib = malloc(sizeof(TrackLibrary));
+
+ if (trackLib) {
+ trackLib->catalog = CreateCatalog();
+ trackLib->index = NULL;
+ trackLib->wordCount = 0;
+ trackLib->trackTypeCount = 0;
+ }
+
+ return (trackLib);
+}
+
+/**
+ * Scan directory and all parameter files found to the catalog
+ *
+ * \param trackLib IN the catalog
+ * \param directory IN directory to scan
+ * \return number of files found
+ */
+
+bool
+GetTrackFiles(TrackLibrary *trackLib, char *directory)
+{
+ ScanDirectory(trackLib->catalog, directory);
+ trackLib->trackTypeCount = CountCatalogEntries(trackLib->catalog);
+
+ return (trackLib->trackTypeCount);
+}
+/**
+ * Add a list of parameters files to a catalog. This function is
+ * called when the user selects files in the file selector.
+ *
+ * \param files IN count of files
+ * \param fileName IN array of filenames
+ * \param data IN pointer to the catalog
+ * \return alwqys TRUE
+ */
+
+int GetParameterFileInfo(
+ int files,
+ char ** fileName,
+ void * data)
+{
+ CatalogEntry *catalog = (CatalogEntry *)data;
+
+ assert(fileName != NULL);
+ assert(files > 0);
+ assert(data != NULL);
+
+ for (int i = 0; i < files; i++) {
+ CatalogEntry *newEntry;
+ char *contents = GetParameterFileContent(fileName[i]);
+
+ if (!(newEntry = IsExistingContents(catalog, contents,TRUE))) {
+ newEntry = InsertIntoCatalogAfter(catalog);
+ }
+ UpdateCatalogEntry(newEntry, fileName[i], contents);
+ free(contents);
+ }
+ return (TRUE);
+}
+
+/**
+ * Create the search index from the contents description for the whole catalog.
+ * A fixed number of words are added to the index. See ESTIMATED_CONTENTS_WORDS
+ *
+ * \param trackLib IN the catalog
+ * \return the number of words indexed
+ */
+
+unsigned
+CreateLibraryIndex(TrackLibrary *trackLib)
+{
+ trackLib->index = CreateIndexTable(trackLib->trackTypeCount *
+ ESTIMATED_CONTENTS_WORDS);
+
+ trackLib->wordCount = CreateContentsIndex(trackLib->catalog, trackLib->index,
+ &trackLib->words_array,
+ ESTIMATED_CONTENTS_WORDS * trackLib->trackTypeCount);
+
+ return (trackLib->wordCount);
+}
+
+void
+DeleteLibraryIndex(TrackLibrary *trackLib)
+{
+ free(trackLib->index);
+ trackLib->index = NULL;
+
+ free(trackLib->words_array);
+
+ trackLib->wordCount = 0;
+
+}
+
+
+/**
+ * Create the library and index of parameter files in a given directory.
+ *
+ * \param directory IN directory to scan
+ * \return NULL if error or empty directory, else library handle
+ */
+
+TrackLibrary *
+CreateLibrary(char *directory)
+{
+ TrackLibrary *library;
+
+ library = InitLibrary();
+ if (library) {
+ if (!GetTrackFiles(library, directory)) {
+ return (NULL);
+ }
+
+ CreateLibraryIndex(library);
+ }
+ return (library);
+}
+
+void
+DeleteLibrary(TrackLibrary* library)
+{
+ DeleteLibraryIndex(library);
+
+
+ free(library);
+}
+
+// Case insensitive comparison
+char* stristr( const char* haystack, const char* needle )
+{
+ int c = tolower((unsigned char)*needle);
+ if (c == '\0')
+ return (char *)haystack;
+ for (; *haystack; haystack++) {
+ if (tolower((unsigned char)*haystack) == c) {
+ for (size_t i = 0;;) {
+ if (needle[++i] == '\0')
+ return (char *)haystack;
+ if (tolower((unsigned char)haystack[i]) != tolower((unsigned char)needle[i]))
+ break;
+ }
+ }
+ }
+ return NULL;
+}
+
+/**
+ * Search the library for a keyword string and return the result list
+ *
+ * First the index is searched for the first word and then each "hit" is matched
+ * to the entire search string
+ *
+ * Null, Blank and "*" match all entries
+ *
+ * The list is de-duped of repeat of filenames as the same file might appear in
+ * more than once
+ *
+ * \param library IN the library
+ * \param searchExpression IN keyword to search for
+ * \param resultEntries IN list header for result list
+ * \return number of found entries
+ */
+unsigned
+SearchLibrary(TrackLibrary *library, char *searchExpression,
+ CatalogEntry *resultEntries)
+{
+ CatalogEntry **entries;
+ CatalogEntry * newEntry = resultEntries;
+ unsigned entryCount;
+
+ char * word;
+
+ word = strdup(searchExpression);
+
+ //word = strtok(word," \t");
+
+ if (library->index == NULL || library->wordCount == 0) {
+ return (0);
+ }
+ entryCount = FindWord(library->index, library->wordCount, word,
+ &entries);
+ int count= 0;
+ if (entryCount) {
+ unsigned int i = 0;
+ while (i < entryCount) {
+ char * match;
+ //Check if entire String Matches
+ if (!searchExpression || !word || (word[0] == '*') || (word[0] == '\0') ||
+ (match = stristr(entries[i]->contents,searchExpression))) {
+ CatalogEntry * existingEntry;
+ existingEntry = IsExistingContents(resultEntries, entries[i]->contents, TRUE);
+ //Same FileName already in one of the entries?
+ BOOL_T found = FALSE;
+ if (existingEntry) {
+ for (unsigned int j=0;j<existingEntry->files;j++) {
+ if (!strcmp(existingEntry->fullFileName[j],entries[i]->fullFileName[entries[i]->files-1])) {
+ found=TRUE;
+ break;
+ }
+ }
+ if (found == TRUE ) {
+ i++;
+ continue;
+ }
+ UpdateCatalogEntry(existingEntry, entries[i]->fullFileName[(entries[i]->files- 1)],
+ entries[i]->contents);
+ } else {
+ newEntry = InsertInOrder(resultEntries,entries[i]->contents);
+ UpdateCatalogEntry(newEntry, entries[i]->fullFileName[(entries[i]->files- 1)],
+ entries[i]->contents);
+ }
+ count++;
+ }
+ i++;
+ }
+ }
+ free(word);
+ if (entries)
+ free(entries); //Clean-up after search
+ return (count);
+}
+
+/**
+ * Get the contents description from a parameter file. Returned string has to be freed after use.
+ *
+ * \param file IN xtpfile
+ * \return pointer to found contents or NULL if not present
+ */
+
+char *
+GetParameterFileContent(char *file)
+{
+ FILE *fh;
+ char *result = NULL;
+
+ fh = fopen(file, "rt");
+ if (fh) {
+ bool found = false;
+
+ while (!found) {
+ char buffer[512];
+ if (fgets(buffer, sizeof(buffer), fh)) {
+ char *ptr = strtok(buffer, " \t");
+ if (!XtcStricmp(ptr, CONTENTSCOMMAND)) {
+ /* if found, store the rest of the line and the filename */
+ ptr = ptr+strlen(CONTENTSCOMMAND)+1;
+ ptr = strtok(ptr, "\r\n");
+ result = strdup(ptr);
+#ifdef WINDOWS
+ ConvertUTF8ToSystem(result);
+#endif // WINDOWS
+ found = true;
+ }
+ } else {
+ fprintf(stderr, "Nothing found in %s\n", file);
+ found = true;
+ }
+ }
+ fclose(fh);
+ }
+ return(result);
+}