/** \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 #include #ifdef HAVE_MALLOC_H #include #endif #include #include #include #include #include #include #include #ifdef WINDOWS #include "include/dirent.h" #else #include #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;jfiles;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); }