diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2020-08-22 14:05:41 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2020-08-22 14:05:41 +0200 |
commit | b55285a77da0e0b829e4ce8d7e09debaabc68e15 (patch) | |
tree | f622559ef65bbdd3e1c5bdb06098a8f89eec0563 /app/bin/partcatalog.c | |
parent | d3897ce090dbeb220ed2c782f095597e417cf3cc (diff) | |
parent | d1ae75703e1ed81d65ea16946dcdb77e7a13adc9 (diff) |
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'app/bin/partcatalog.c')
-rw-r--r-- | app/bin/partcatalog.c | 846 |
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); +} |