/* * Copyright (c) 2000, 2001 by Martin C. Shepherd. * * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Except as contained in this notice, the name of a copyright holder * shall not be used in advertising or otherwise to promote the sale, use * or other dealings in this Software without prior written authorization * of the copyright holder. */ #include #include #include #include #include #include #include "history.h" #include "freelist.h" /* * GlLineNode's record the location and length of historical lines in * a buffer array. */ typedef struct GlLineNode GlLineNode; struct GlLineNode { long id; /* The unique identifier of this history line */ time_t timestamp; /* The time at which the line was archived */ unsigned group; /* The identifier of the history group to which the */ /* the line belongs. */ GlLineNode *next; /* The next youngest line in the list */ GlLineNode *prev; /* The next oldest line in the list */ int start; /* The start index of the line in the buffer */ int nchar; /* The total length of the line, including the '\0' */ }; /* * The number of GlLineNode elements per freelist block. */ #define LINE_NODE_BLK 100 /* * Lines are organised in the buffer from oldest to newest. The * positions of the lines are recorded in a doubly linked list * of GlLineNode objects. */ typedef struct { FreeList *node_mem; /* A freelist of GlLineNode objects */ GlLineNode *head; /* The head of the list of lines */ GlLineNode *tail; /* The tail of the list of lines */ } GlLineList; /* * All elements of the history mechanism are recorded in an object of * the following type. */ struct GlHistory { char *buffer; /* A circular buffer used to record historical input */ /* lines. */ size_t buflen; /* The length of the buffer array */ GlLineList list; /* A list of the start of lines in buffer[] */ GlLineNode *recall; /* The last line recalled, or NULL if no recall */ /* session is currently active. */ GlLineNode *id_node;/* The node at which the last ID search terminated */ const char *prefix; /* A pointer to the line containing the prefix that */ /* is being searched for. */ int prefix_len; /* The length of the prefix */ unsigned long seq; /* The next ID to assign to a line node */ unsigned group; /* The identifier of the current history group */ int nline; /* The number of lines currently in the history list */ int max_lines; /* Either -1 or a ceiling on the number of lines */ int enable; /* If false, ignore history additions and lookups */ }; static char *_glh_restore_line(GlHistory *glh, char *line, size_t dim); static int _glh_cant_load_history(GlHistory *glh, const char *filename, int lineno, const char *message, FILE *fp); static int _glh_write_timestamp(FILE *fp, time_t timestamp); static int _glh_decode_timestamp(char *string, char **endp, time_t *t); static void _glh_discard_node(GlHistory *glh, GlLineNode *node); static GlLineNode *_glh_find_id(GlHistory *glh, GlhLineID id); /*....................................................................... * Create a line history maintenance object. * * Input: * buflen size_t The number of bytes to allocate to the circular * buffer that is used to record all of the * most recent lines of user input that will fit. * If buflen==0, no buffer will be allocated. * Output: * return GlHistory * The new object, or NULL on error. */ GlHistory *_new_GlHistory(size_t buflen) { GlHistory *glh; /* The object to be returned */ /* * Allocate the container. */ glh = (GlHistory *) malloc(sizeof(GlHistory)); if(!glh) { fprintf(stderr, "_new_GlHistory: Insufficient memory.\n"); return NULL; }; /* * Before attempting any operation that might fail, initialize the * container at least up to the point at which it can safely be passed * to _del_GlHistory(). */ glh->buffer = NULL; glh->buflen = buflen; glh->list.node_mem = NULL; glh->list.head = NULL; glh->list.tail = NULL; glh->recall = NULL; glh->id_node = NULL; glh->prefix = NULL; glh->prefix_len = 0; glh->seq = 0; glh->group = 0; glh->nline = 0; glh->max_lines = -1; glh->enable = 1; /* * Allocate the buffer, if required. */ if(buflen > 0) { glh->buffer = (char *) malloc(sizeof(char) * buflen); if(!glh->buffer) { fprintf(stderr, "_new_GlHistory: Insufficient memory.\n"); return _del_GlHistory(glh); }; }; /* * Allocate the GlLineNode freelist. */ glh->list.node_mem = _new_FreeList("_new_GlHistory", sizeof(GlLineNode), LINE_NODE_BLK); if(!glh->list.node_mem) return _del_GlHistory(glh); return glh; } /*....................................................................... * Delete a GlHistory object. * * Input: * glh GlHistory * The object to be deleted. * Output: * return GlHistory * The deleted object (always NULL). */ GlHistory *_del_GlHistory(GlHistory *glh) { if(glh) { /* * Delete the buffer. */ if(glh->buffer) { free(glh->buffer); glh->buffer = NULL; }; /* * Delete the freelist of GlLineNode's. */ glh->list.node_mem = _del_FreeList("_del_GlHistory", glh->list.node_mem, 1); /* * The contents of the list were deleted by deleting the freelist. */ glh->list.head = NULL; glh->list.tail = NULL; /* * Delete the container. */ free(glh); }; return NULL; } /*....................................................................... * Add a new line to the end of the history buffer, wrapping round to the * start of the buffer if needed. * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The line to be archived. * force int Unless this flag is non-zero, empty lines and * lines which match the previous line in the history * buffer, aren't archived. This flag requests that * the line be archived regardless. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_add_history(GlHistory *glh, const char *line, int force) { GlLineList *list; /* The line location list */ int nchar; /* The number of characters needed to record the line */ GlLineNode *node; /* The new line location list node */ int empty; /* True if the string is empty */ const char *nlptr;/* A pointer to a newline character in line[] */ int i; /* * Check the arguments. */ if(!glh || !line) return 1; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return 0; /* * Get the line location list. */ list = &glh->list; /* * Cancel any ongoing search. */ if(_glh_cancel_search(glh)) return 1; /* * See how much buffer space will be needed to record the line? * * If the string contains a terminating newline character, arrange to * have the archived line NUL terminated at this point. */ nlptr = strchr(line, '\n'); if(nlptr) nchar = (nlptr - line) + 1; else nchar = strlen(line) + 1; /* * If the line is too big to fit in the buffer, truncate it. */ if(nchar > glh->buflen) nchar = glh->buflen; /* * Is the line empty? */ empty = 1; for(i=0; itail && strlen(glh->buffer + list->tail->start) == nchar-1 && strncmp(line, glh->buffer + list->tail->start, nchar-1)==0) return 0; /* * Allocate the list node that will record the line location. */ node = (GlLineNode *) _new_FreeListNode(list->node_mem); if(!node) return 1; /* * Is the buffer empty? */ if(!list->head) { /* * Place the line at the beginning of the buffer. */ strncpy(glh->buffer, line, nchar); glh->buffer[nchar-1] = '\0'; /* * Record the location of the line. */ node->start = 0; /* * The buffer has one or more lines in it. */ } else { /* * Place the start of the new line just after the most recently * added line. */ int start = list->tail->start + list->tail->nchar; /* * If there is insufficient room between the end of the most * recently added line and the end of the buffer, we place the * line at the beginning of the buffer. To make as much space * as possible for this line, we first delete any old lines * at the end of the buffer, then shift the remaining contents * of the buffer to the end of the buffer. */ if(start + nchar >= glh->buflen) { GlLineNode *last; /* The last line in the buffer */ GlLineNode *ln; /* A member of the list of line locations */ int shift; /* The shift needed to move the contents of the */ /* buffer to its end. */ /* * Delete any old lines between the most recent line and the end of the * buffer. */ while(list->head && list->head->start > list->tail->start) _glh_discard_node(glh, list->head); /* * Find the line that is nearest the end of the buffer. */ last = NULL; for(ln=list->head; ln; ln=ln->next) { if(!last || ln->start > last->start) last = ln; }; /* * How big a shift is needed to move the existing contents of the * buffer to the end of the buffer? */ shift = last ? (glh->buflen - (last->start + last->nchar)) : 0; /* * Is any shift needed? */ if(shift > 0) { /* * Move the buffer contents to the end of the buffer. */ memmove(glh->buffer + shift, glh->buffer, glh->buflen - shift); /* * Update the listed locations to reflect the shift. */ for(ln=list->head; ln; ln=ln->next) ln->start += shift; }; /* * The new line should now be located at the start of the buffer. */ start = 0; }; /* * Make space for the new line at the beginning of the buffer by * deleting the oldest lines. This just involves removing them * from the list of used locations. Also enforce the current * maximum number of lines. */ while(list->head && ((list->head->start >= start && list->head->start - start < nchar) || (glh->max_lines >= 0 && glh->nline>=glh->max_lines))) { _glh_discard_node(glh, list->head); }; /* * Copy the new line into the buffer. */ memcpy(glh->buffer + start, line, nchar); glh->buffer[start + nchar - 1] = '\0'; /* * Record its location. */ node->start = start; }; /* * Append the line location node to the end of the list. */ node->id = glh->seq++; node->timestamp = time(NULL); node->group = glh->group; node->nchar = nchar; node->next = NULL; node->prev = list->tail; if(list->tail) list->tail->next = node; else list->head = node; list->tail = node; glh->nline++; return 0; } /*....................................................................... * Recall the next oldest line that has the search prefix last recorded * by _glh_search_prefix(). * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The input line buffer. On input this should contain * the current input line, and on output, if anything * was found, its contents will have been replaced * with the matching line. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * A pointer to line[0], or NULL if not found. */ char *_glh_find_backwards(GlHistory *glh, char *line, size_t dim) { GlLineNode *node; /* The line location node being checked */ int first; /* True if this is the start of a new search */ /* * Check the arguments. */ if(!glh || !line) { fprintf(stderr, "_glh_find_backwards: NULL argument(s).\n"); return NULL; }; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return NULL; /* * Check the line dimensions. */ if(dim < strlen(line) + 1) { fprintf(stderr, "_glh_find_backwards: 'dim' inconsistent with strlen(line) contents.\n"); return NULL; }; /* * Is this the start of a new search? */ first = glh->recall==NULL; /* * If this is the first search backwards, save the current line * for potential recall later, and mark it as the last line * recalled. */ if(first) { if(_glh_add_history(glh, line, 1)) return NULL; glh->recall = glh->list.tail; }; /* * If there is no search prefix, the prefix last set by glh_search_prefix() * doesn't exist in the history buffer. */ if(!glh->prefix) return NULL; /* * From where should we start the search? */ if(glh->recall) node = glh->recall->prev; else node = glh->list.tail; /* * Search backwards through the list for the first match with the * prefix string. */ for( ; node && (node->group != glh->group || strncmp(glh->buffer + node->start, glh->prefix, glh->prefix_len) != 0); node = node->prev) ; /* * Was a matching line found? */ if(node) { /* * Recall the found node as the starting point for subsequent * searches. */ glh->recall = node; /* * Copy the matching line into the provided line buffer. */ strncpy(line, glh->buffer + node->start, dim); line[dim-1] = '\0'; return line; }; /* * No match was found. */ return NULL; } /*....................................................................... * Recall the next newest line that has the search prefix last recorded * by _glh_search_prefix(). * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The input line buffer. On input this should contain * the current input line, and on output, if anything * was found, its contents will have been replaced * with the matching line. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * The line requested, or NULL if no matching line * was found. */ char *_glh_find_forwards(GlHistory *glh, char *line, size_t dim) { GlLineNode *node; /* The line location node being checked */ /* * Check the arguments. */ if(!glh || !line) { fprintf(stderr, "_glh_find_forwards: NULL argument(s).\n"); return NULL; }; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return NULL; /* * Check the line dimensions. */ if(dim < strlen(line) + 1) { fprintf(stderr, "_glh_find_forwards: 'dim' inconsistent with strlen(line) contents.\n"); return NULL; }; /* * From where should we start the search? */ if(glh->recall) node = glh->recall->next; else return NULL; /* * If there is no search prefix, the prefix last set by glh_search_prefix() * doesn't exist in the history buffer. */ if(!glh->prefix) return NULL; /* * Search forwards through the list for the first match with the * prefix string. */ for( ; node && (node->group != glh->group || strncmp(glh->buffer + node->start, glh->prefix, glh->prefix_len) != 0); node = node->next) ; /* * Was a matching line found? */ if(node) { /* * Did we hit the line that was originally being edited when the * current history traversal started? */ if(node == glh->list.tail) return _glh_restore_line(glh, line, dim); /* * Copy the matching line into the provided line buffer. */ strncpy(line, glh->buffer + node->start, dim); line[dim-1] = '\0'; /* * Record the starting point of the next search. */ glh->recall = node; /* * Return the matching line to the user. */ return line; }; /* * No match was found. */ return NULL; } /*....................................................................... * If a search is in progress, cancel it. * * This involves discarding the line that was temporarily saved by * _glh_find_backwards() when the search was originally started, * and reseting the search iteration pointer to NULL. * * Input: * glh GlHistory * The input-line history maintenance object. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_cancel_search(GlHistory *glh) { /* * Check the arguments. */ if(!glh) { fprintf(stderr, "_glh_cancel_search: NULL argument(s).\n"); return 1; }; /* * If there wasn't a search in progress, do nothing. */ if(!glh->recall) return 0; /* * Delete the node of the preserved line. */ _glh_discard_node(glh, glh->list.tail); /* * Reset the search pointers. */ glh->recall = NULL; glh->prefix = ""; glh->prefix_len = 0; return 0; } /*....................................................................... * Set the prefix of subsequent history searches. * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The command line who's prefix is to be used. * prefix_len int The length of the prefix. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_search_prefix(GlHistory *glh, const char *line, int prefix_len) { GlLineNode *node; /* The line location node being checked */ /* * Check the arguments. */ if(!glh) { fprintf(stderr, "_glh_search_prefix: NULL argument(s).\n"); return 1; }; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return 0; /* * Record a zero length search prefix? */ if(prefix_len <= 0) { glh->prefix_len = 0; glh->prefix = ""; return 0; }; /* * Record the length of the new search prefix. */ glh->prefix_len = prefix_len; /* * If any history line starts with the specified prefix, record a * pointer to it for comparison in subsequent searches. If the prefix * doesn't match any of the lines, then simply record NULL to indicate * that there is no point in searching. Note that _glh_add_history() * clears this pointer by calling _glh_cancel_search(), so there is * no danger of it being used after the buffer has been modified. */ for(node = glh->list.tail ; node && (node->group != glh->group || strncmp(glh->buffer + node->start, line, prefix_len) != 0); node = node->prev) ; /* * If a matching line was found record it for use as the search * prefix. */ glh->prefix = node ? glh->buffer + node->start : NULL; return 0; } /*....................................................................... * Recall the oldest recorded line. * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The input line buffer. On input this should contain * the current input line, and on output, its contents * will have been replaced with the oldest line. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * A pointer to line[0], or NULL if not found. */ char *_glh_oldest_line(GlHistory *glh, char *line, size_t dim) { GlLineNode *node; /* The line location node being checked */ int first; /* True if this is the start of a new search */ /* * Check the arguments. */ if(!glh || !line) { fprintf(stderr, "_glh_oldest_line: NULL argument(s).\n"); return NULL; }; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return NULL; /* * Check the line dimensions. */ if(dim < strlen(line) + 1) { fprintf(stderr, "_glh_oldest_line: 'dim' inconsistent with strlen(line) contents.\n"); return NULL; }; /* * Is this the start of a new search? */ first = glh->recall==NULL; /* * If this is the first search backwards, save the current line * for potential recall later, and mark it as the last line * recalled. */ if(first) { if(_glh_add_history(glh, line, 1)) return NULL; glh->recall = glh->list.tail; }; /* * Locate the oldest line that belongs to the current group. */ for(node=glh->list.head; node && node->group != glh->group; node = node->next) ; /* * No line found? */ if(!node) return NULL; /* * Record the above node as the starting point for subsequent * searches. */ glh->recall = node; /* * Copy the recalled line into the provided line buffer. */ strncpy(line, glh->buffer + node->start, dim); line[dim-1] = '\0'; return line; } /*....................................................................... * Recall the line that was being entered when the search started. * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The input line buffer. On input this should contain * the current input line, and on output, its contents * will have been replaced with the line that was * being entered when the search was started. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * A pointer to line[0], or NULL if not found. */ char *_glh_current_line(GlHistory *glh, char *line, size_t dim) { /* * Check the arguments. */ if(!glh || !line) { fprintf(stderr, "_glh_current_line: NULL argument(s).\n"); return NULL; }; /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return NULL; /* * Check the line dimensions. */ if(dim < strlen(line) + 1) { fprintf(stderr, "_glh_current_line: 'dim' inconsistent with strlen(line) contents.\n"); return NULL; }; /* * Restore the original line. */ return _glh_restore_line(glh, line, dim); } /*....................................................................... * Remove the line that was originally being edited when the history * traversal was started, from its saved position in the history list, * and place it in the provided line buffer. * * Input: * glh GlHistory * The input-line history maintenance object. * line char * The input line buffer. On input this should contain * the current input line, and on output, its contents * will have been replaced with the saved line. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * A pointer to line[0], or NULL if not found. */ static char *_glh_restore_line(GlHistory *glh, char *line, size_t dim) { GlLineNode *tail; /* The tail node to be discarded */ /* * If there wasn't a search in progress, do nothing. */ if(!glh->recall) return NULL; /* * Get the list node that is to be removed. */ tail = glh->list.tail; /* * If a pointer to the saved line is being used to record the * current search prefix, reestablish the search prefix, to * have it recorded by another history line if possible. */ if(glh->prefix == glh->buffer + tail->start) (void) _glh_search_prefix(glh, glh->buffer + tail->start, glh->prefix_len); /* * Copy the recalled line into the input-line buffer. */ strncpy(line, glh->buffer + tail->start, dim); line[dim-1] = '\0'; /* * Discard the line-location node. */ _glh_discard_node(glh, tail); /* * Mark the search as ended. */ glh->recall = NULL; return line; } /*....................................................................... * Query the id of a history line offset by a given number of lines from * the one that is currently being recalled. If a recall session isn't * in progress, or the offset points outside the history list, 0 is * returned. * * Input: * glh GlHistory * The input-line history maintenance object. * offset int The line offset (0 for the current line, < 0 * for an older line, > 0 for a newer line. * Output: * return GlhLineID The identifier of the line that is currently * being recalled, or 0 if no recall session is * currently in progress. */ GlhLineID _glh_line_id(GlHistory *glh, int offset) { GlLineNode *node; /* The line location node being checked */ /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return 0; /* * Search forward 'offset' lines to find the required line. */ if(offset >= 0) { for(node=glh->recall; node && offset != 0; node=node->next) { if(node->group == glh->group) offset--; }; } else { for(node=glh->recall; node && offset != 0; node=node->prev) { if(node->group == glh->group) offset++; }; }; return node ? node->id : 0; } /*....................................................................... * Recall a line by its history buffer ID. If the line is no longer * in the buffer, or the id is zero, NULL is returned. * * Input: * glh GlHistory * The input-line history maintenance object. * id GlhLineID The ID of the line to be returned. * line char * The input line buffer. On input this should contain * the current input line, and on output, its contents * will have been replaced with the saved line. * dim size_t The allocated dimensions of the line buffer. * Output: * return char * A pointer to line[0], or NULL if not found. */ char *_glh_recall_line(GlHistory *glh, GlhLineID id, char *line, size_t dim) { GlLineNode *node; /* The line location node being checked */ /* * Is history enabled? */ if(!glh->enable || !glh->buffer || glh->max_lines == 0) return NULL; /* * If we are starting a new recall session, save the current line * for potential recall later. */ if(!glh->recall && _glh_add_history(glh, line, 1)) return NULL; /* * Search for the specified line. */ node = _glh_find_id(glh, id); /* * Not found? */ if(!node || node->group != glh->group) return NULL; /* * Record the node of the matching line as the starting point * for subsequent searches. */ glh->recall = node; /* * Copy the recalled line into the provided line buffer. */ strncpy(line, glh->buffer + node->start, dim); line[dim-1] = '\0'; return line; } /*....................................................................... * Save the current history in a specified file. * * Input: * glh GlHistory * The input-line history maintenance object. * filename const char * The name of the new file to record the * history in. * comment const char * Extra information such as timestamps will * be recorded on a line started with this * string, the idea being that the file can * double as a command file. Specify "" if * you don't care. * max_lines int The maximum number of lines to save, or -1 * to save all of the lines in the history * list. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_save_history(GlHistory *glh, const char *filename, const char *comment, int max_lines) { FILE *fp; /* The output file */ GlLineNode *node; /* The line being saved */ GlLineNode *head; /* The head of the list of lines to be saved */ /* * Check the arguments. */ if(!glh || !filename || !comment) { fprintf(stderr, "_glh_save_history: NULL argument(s).\n"); return 1; }; /* * Attempt to open the specified file. */ fp = fopen(filename, "w"); if(!fp) { fprintf(stderr, "_glh_save_history: Can't open %s (%s).\n", filename, strerror(errno)); return 1; }; /* * If a ceiling on the number of lines to save was specified, count * that number of lines backwards, to find the first line to be saved. */ head = NULL; if(max_lines >= 0) { for(head=glh->list.tail; head && --max_lines > 0; head=head->prev) ; }; if(!head) head = glh->list.head; /* * Write the contents of the history buffer to the history file, writing * associated data such as timestamps, to a line starting with the * specified comment string. */ for(node=head; node; node=node->next) { /* * Write peripheral information associated with the line, as a comment. */ if(fprintf(fp, "%s ", comment) < 0 || _glh_write_timestamp(fp, node->timestamp) || fprintf(fp, " %u\n", node->group) < 0) { fprintf(stderr, "Error writing %s (%s).\n", filename, strerror(errno)); (void) fclose(fp); return 1; }; /* * Write the history line. */ if(fprintf(fp, "%s\n", glh->buffer + node->start) < 0) { fprintf(stderr, "Error writing %s (%s).\n", filename, strerror(errno)); (void) fclose(fp); return 1; }; }; /* * Close the history file. */ if(fclose(fp) == EOF) { fprintf(stderr, "Error writing %s (%s).\n", filename, strerror(errno)); return 1; }; return 0; } /*....................................................................... * Restore previous history lines from a given file. * * Input: * glh GlHistory * The input-line history maintenance object. * filename const char * The name of the file to read from. * comment const char * The same comment string that was passed to * _glh_save_history() when this file was * written. * line char * A buffer into which lines can be read. * dim size_t The allocated dimension of line[]. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_load_history(GlHistory *glh, const char *filename, const char *comment, char *line, size_t dim) { FILE *fp; /* The output file */ size_t comment_len; /* The length of the comment string */ time_t timestamp; /* The timestamp of the history line */ unsigned group; /* The identifier of the history group to which */ /* the line belongs. */ int lineno; /* The line number being read */ /* * Check the arguments. */ if(!glh || !filename || !comment || !line) { fprintf(stderr, "_glh_load_history: NULL argument(s).\n"); return 1; }; /* * Measure the length of the comment string. */ comment_len = strlen(comment); /* * Clear the history list. */ _glh_clear_history(glh, 1); /* * Attempt to open the specified file. Don't treat it as an error * if the file doesn't exist. */ fp = fopen(filename, "r"); if(!fp) return 0; /* * Attempt to read each line and preceding peripheral info, and add these * to the history list. */ for(lineno=1; fgets(line, dim, fp) != NULL; lineno++) { char *lptr; /* A pointer into the input line */ /* * Check that the line starts with the comment string. */ if(strncmp(line, comment, comment_len) != 0) { return _glh_cant_load_history(glh, filename, lineno, "Corrupt history parameter line", fp); }; /* * Skip spaces and tabs after the comment. */ for(lptr=line+comment_len; *lptr && (*lptr==' ' || *lptr=='\t'); lptr++) ; /* * The next word must be a timestamp. */ if(_glh_decode_timestamp(lptr, &lptr, ×tamp)) { return _glh_cant_load_history(glh, filename, lineno, "Corrupt timestamp", fp); }; /* * Skip spaces and tabs. */ while(*lptr==' ' || *lptr=='\t') lptr++; /* * The next word must be an unsigned integer group number. */ group = (int) strtoul(lptr, &lptr, 10); if(*lptr != ' ' && *lptr != '\n') { return _glh_cant_load_history(glh, filename, lineno, "Corrupt group id", fp); }; /* * Skip spaces and tabs. */ while(*lptr==' ' || *lptr=='\t') lptr++; /* * There shouldn't be anything left on the line. */ if(*lptr != '\n') { return _glh_cant_load_history(glh, filename, lineno, "Corrupt parameter line", fp); }; /* * Now read the history line itself. */ lineno++; if(fgets(line, dim, fp) == NULL) return _glh_cant_load_history(glh, filename, lineno, "Read error", fp); /* * Append the line to the history buffer. */ if(_glh_add_history(glh, line, 1)) { return _glh_cant_load_history(glh, filename, lineno, "Insufficient memory to record line", fp); }; /* * Record the group and timestamp information along with the line. */ if(glh->list.tail) { glh->list.tail->timestamp = timestamp; glh->list.tail->group = group; }; }; /* * Close the file. */ (void) fclose(fp); return 0; } /*....................................................................... * This is a private error return function of _glh_load_history(). */ static int _glh_cant_load_history(GlHistory *glh, const char *filename, int lineno, const char *message, FILE *fp) { fprintf(stderr, "%s:%d: %s.\n", filename, lineno, message); (void) fclose(fp); return 1; } /*....................................................................... * Switch history groups. * * Input: * glh GlHistory * The input-line history maintenance object. * group unsigned The new group identifier. This will be recorded * with subsequent history lines, and subsequent * history searches will only return lines with * this group identifier. This allows multiple * separate history lists to exist within * a single GlHistory object. Note that the * default group identifier is 0. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_set_group(GlHistory *glh, unsigned group) { /* * Check the arguments. */ if(!glh) { fprintf(stderr, "_glh_set_group: NULL argument(s).\n"); return 1; }; /* * Is the group being changed? */ if(group != glh->group) { /* * Cancel any ongoing search. */ if(_glh_cancel_search(glh)) return 1; /* * Record the new group. */ glh->group = group; }; return 0; } /*....................................................................... * Query the current history group. * * Input: * glh GlHistory * The input-line history maintenance object. * Output: * return unsigned The group identifier. */ int _glh_get_group(GlHistory *glh) { return glh ? glh->group : 0; } /*....................................................................... * Write a timestamp to a given stdio stream, in the format * yyyymmddhhmmss * * Input: * fp FILE * The stream to write to. * timestamp time_t The timestamp to be written. * Output: * return int 0 - OK. * 1 - Error. */ static int _glh_write_timestamp(FILE *fp, time_t timestamp) { struct tm *t; /* THe broken-down calendar time */ /* * Get the calendar components corresponding to the given timestamp. */ if(timestamp < 0 || (t = localtime(×tamp)) == NULL) { if(fprintf(fp, "?") < 0) return 1; return 0; }; /* * Write the calendar time as yyyymmddhhmmss. */ if(fprintf(fp, "%04d%02d%02d%02d%02d%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec) < 0) return 1; return 0; } /*....................................................................... * Read a timestamp from a string. * * Input: * string char * The string to read from. * Input/Output: * endp char ** On output *endp will point to the next unprocessed * character in string[]. * timestamp time_t * The timestamp will be assigned to *t. * Output: * return int 0 - OK. * 1 - Error. */ static int _glh_decode_timestamp(char *string, char **endp, time_t *timestamp) { unsigned year,month,day,hour,min,sec; /* Calendar time components */ struct tm t; /* * There are 14 characters in the date format yyyymmddhhmmss. */ enum {TSLEN=14}; char timestr[TSLEN+1]; /* The timestamp part of the string */ /* * If the time wasn't available at the time that the line was recorded * it will have been written as "?". Check for this before trying * to read the timestamp. */ if(string[0] == '\?') { *endp = string+1; *timestamp = -1; return 0; }; /* * The timestamp is expected to be written in the form yyyymmddhhmmss. */ if(strlen(string) < TSLEN) { *endp = string; return 1; }; /* * Copy the timestamp out of the string. */ strncpy(timestr, string, TSLEN); timestr[TSLEN] = '\0'; /* * Decode the timestamp. */ if(sscanf(timestr, "%4u%2u%2u%2u%2u%2u", &year, &month, &day, &hour, &min, &sec) != 6) { *endp = string; return 1; }; /* * Advance the string pointer over the successfully read timestamp. */ *endp = string + TSLEN; /* * Copy the read values into a struct tm. */ t.tm_sec = sec; t.tm_min = min; t.tm_hour = hour; t.tm_mday = day; t.tm_wday = 0; t.tm_yday = 0; t.tm_mon = month - 1; t.tm_year = year - 1900; t.tm_isdst = -1; /* * Convert the contents of the struct tm to a time_t. */ *timestamp = mktime(&t); return 0; } /*....................................................................... * Display the contents of the history list. * * Input: * glh GlHistory * The input-line history maintenance object. * fp FILE * The stdio stream to write to. * fmt const char * A format string. This can contain arbitrary * characters, which are written verbatim, plus * any of the following format directives: * %D - The date, like 2001-11-20 * %T - The time of day, like 23:59:59 * %N - The sequential entry number of the * line in the history buffer. * %G - The history group number of the line. * %% - A literal % character. * %H - The history line. * all_groups int If true, display history lines from all * history groups. Otherwise only display * those of the current history group. * max_lines int If max_lines is < 0, all available lines * are displayed. Otherwise only the most * recent max_lines lines will be displayed. * Output: * return int 0 - OK. * 1 - Error. */ int _glh_show_history(GlHistory *glh, FILE *fp, const char *fmt, int all_groups, int max_lines) { GlLineNode *node; /* The line being displayed */ GlLineNode *oldest; /* The oldest line to display */ enum {TSMAX=32}; /* The maximum length of the date and time string */ char buffer[TSMAX+1]; /* The buffer in which to write the date and time */ int idlen; /* The length of displayed ID strings */ unsigned grpmax; /* The maximum group number in the buffer */ int grplen; /* The number of characters needed to print grpmax */ /* * Check the arguments. */ if(!glh || !fp || !fmt) { fprintf(stderr, "_glh_show_history: NULL argument(s).\n"); return 1; }; /* * Is history enabled? */ if(!glh->enable || !glh->list.head) return 0; /* * Work out the length to display ID numbers, choosing the length of * the biggest number in the buffer. Smaller numbers will be padded * with leading zeroes if needed. */ sprintf(buffer, "%lu", (unsigned long) glh->list.tail->id); idlen = strlen(buffer); /* * Find the largest group number. */ grpmax = 0; for(node=glh->list.head; node; node=node->next) { if(node->group > grpmax) grpmax = node->group; }; /* * Find out how many characters are needed to display the group number. */ sprintf(buffer, "%u", (unsigned) grpmax); grplen = strlen(buffer); /* * Find the node that follows the oldest line to be displayed. */ if(max_lines < 0) { oldest = glh->list.head; } else if(max_lines==0) { return 0; } else { for(oldest=glh->list.tail; oldest; oldest=oldest->prev) { if((all_groups || oldest->group == glh->group) && --max_lines <= 0) break; }; /* * If the number of lines in the buffer doesn't exceed the specified * maximum, start from the oldest line in the buffer. */ if(!oldest) oldest = glh->list.head; }; /* * List the history lines in increasing time order. */ for(node=oldest; node; node=node->next) { /* * Only display lines from the current history group, unless * told otherwise. */ if(all_groups || node->group == glh->group) { const char *fptr; /* A pointer into the format string */ struct tm *t = NULL; /* The broken time version of the timestamp */ /* * Work out the calendar representation of the node timestamp. */ if(node->timestamp != (time_t) -1) t = localtime(&node->timestamp); /* * Parse the format string. */ fptr = fmt; while(*fptr) { /* * Search for the start of the next format directive or the end of the string. */ const char *start = fptr; while(*fptr && *fptr != '%') fptr++; /* * Display any literal characters that precede the located directive. */ if(fptr > start && fprintf(fp, "%.*s", (int) (fptr - start), start) < 0) return 1; /* * Did we hit a new directive before the end of the line? */ if(*fptr) { /* * Obey the directive. Ignore unknown directives. */ switch(*++fptr) { case 'D': /* Display the date */ if(t && strftime(buffer, TSMAX, "%Y-%m-%d", t) != 0 && fprintf(fp, "%s", buffer) < 0) return 1; break; case 'T': /* Display the time of day */ if(t && strftime(buffer, TSMAX, "%H:%M:%S", t) != 0 && fprintf(fp, "%s", buffer) < 0) return 1; break; case 'N': /* Display the sequential entry number */ if(fprintf(fp, "%*lu", idlen, (unsigned long) node->id) < 0) return 1; break; case 'G': if(fprintf(fp, "%*u", grplen, (unsigned) node->group) < 0) return 1; break; case 'H': /* Display the history line */ if(fprintf(fp, "%s", glh->buffer + node->start) < 0) return 1; break; case '%': /* A literal % symbol */ if(fputc('%', fp) == EOF) return 1; break; }; /* * Skip the directive. */ if(*fptr) fptr++; }; }; }; }; return 0; } /*....................................................................... * Change the size of the history buffer. * * Input: * glh GlHistory * The input-line history maintenance object. * bufsize size_t The number of bytes in the history buffer, or 0 * to delete the buffer completely. * Output: * return int 0 - OK. * 1 - Insufficient memory (the previous buffer * will have been retained). No error message * will be displayed. */ int _glh_resize_history(GlHistory *glh, size_t bufsize) { GlLineNode *node; /* A line location node in the list of lines */ GlLineNode *prev; /* The line location node preceding 'node' */ /* * Check the arguments. */ if(!glh) return bufsize > 0; /* * If the new size doesn't differ from the existing size, do nothing. */ if(glh->buflen == bufsize) return 0; /* * Cancel any ongoing search. */ (void) _glh_cancel_search(glh); /* * Create a wholly new buffer? */ if(glh->buflen == 0) { glh->buffer = (char *) malloc(bufsize); if(!glh->buffer) return 1; glh->buflen = bufsize; /* * Delete an existing buffer? */ } else if(bufsize == 0) { _glh_clear_history(glh, 1); free(glh->buffer); glh->buffer = NULL; glh->buflen = 0; /* * To get here, we must be shrinking or expanding from one * finite size to another. */ } else { /* * If we are shrinking the size of the buffer, then we first need * to discard the oldest lines that won't fit in the new buffer. */ if(bufsize < glh->buflen) { size_t nbytes = 0; /* The number of bytes used in the new buffer */ GlLineNode *oldest; /* The oldest node to be kept */ /* * Searching backwards from the youngest line, find the oldest * line for which there will be sufficient room in the new buffer. */ for(oldest = glh->list.tail; oldest && (nbytes += oldest->nchar) <= bufsize; oldest = oldest->prev) ; /* * We will have gone one node too far, unless we reached the oldest line * without exceeding the target length. */ if(oldest) { nbytes -= oldest->nchar; oldest = oldest->next; }; /* * Discard the nodes that can't be retained. */ while(glh->list.head && glh->list.head != oldest) _glh_discard_node(glh, glh->list.head); /* * If we are increasing the size of the buffer, we need to reallocate * the buffer before shifting the lines into their new positions. */ } else { char *new_buffer = (char *) realloc(glh->buffer, bufsize); if(!new_buffer) return 1; glh->buffer = new_buffer; glh->buflen = bufsize; }; /* * If there are any lines to be preserved, copy the block of lines * that precedes the end of the existing buffer to what will be * the end of the new buffer. */ if(glh->list.head) { int shift; /* The number of bytes to shift lines in the buffer */ /* * Get the oldest line to be kept. */ GlLineNode *oldest = glh->list.head; /* * Count the number of characters that are used in the lines that * precede the end of the current buffer (ie. not including those * lines that have been wrapped to the start of the buffer). */ int n = 0; for(node=oldest,prev=oldest->prev; node && node->start >= oldest->start; prev=node, node=node->next) n += node->nchar; /* * Move these bytes to the end of the resized buffer. */ memmove(glh->buffer + bufsize - n, glh->buffer + oldest->start, n); /* * Adjust the buffer pointers to reflect the new locations of the moved * lines. */ shift = bufsize - n - oldest->start; for(node=prev; node && node->start >= oldest->start; node=node->prev) node->start += shift; }; /* * Shrink the buffer? */ if(bufsize < glh->buflen) { char *new_buffer = (char *) realloc(glh->buffer, bufsize); if(new_buffer) glh->buffer = new_buffer; glh->buflen = bufsize; /* Mark it as shrunk, regardless of success */ }; }; return 0; } /*....................................................................... * Set an upper limit to the number of lines that can be recorded in the * history list, or remove a previously specified limit. * * Input: * glh GlHistory * The input-line history maintenance object. * max_lines int The maximum number of lines to allow, or -1 to * cancel a previous limit and allow as many lines * as will fit in the current history buffer size. */ void _glh_limit_history(GlHistory *glh, int max_lines) { if(!glh) return; /* * Apply a new limit? */ if(max_lines >= 0 && max_lines != glh->max_lines) { /* * Count successively older lines until we reach the start of the * list, or until we have seen max_lines lines (at which point 'node' * will be line number max_lines+1). */ int nline = 0; GlLineNode *node; for(node=glh->list.tail; node && ++nline <= max_lines; node=node->prev) ; /* * Discard any lines that exceed the limit. */ if(node) { GlLineNode *oldest = node->next; /* The oldest line to be kept */ /* * Delete nodes from the head of the list until we reach the node that * is to be kept. */ while(glh->list.head && glh->list.head != oldest) _glh_discard_node(glh, glh->list.head); }; }; /* * Record the new limit. */ glh->max_lines = max_lines; return; } /*....................................................................... * Discard either all history, or the history associated with the current * history group. * * Input: * glh GlHistory * The input-line history maintenance object. * all_groups int If true, clear all of the history. If false, * clear only the stored lines associated with the * currently selected history group. */ void _glh_clear_history(GlHistory *glh, int all_groups) { /* * Check the arguments. */ if(!glh) return; /* * Cancel any ongoing search. */ (void) _glh_cancel_search(glh); /* * Delete all history lines regardless of group? */ if(all_groups) { _rst_FreeList(glh->list.node_mem); glh->list.head = glh->list.tail = NULL; glh->nline = 0; glh->id_node = NULL; /* * Just delete lines of the current group? */ } else { GlLineNode *node; /* The line node being checked */ GlLineNode *prev; /* The line node that precedes 'node' */ GlLineNode *next; /* The line node that follows 'node' */ /* * Search out and delete the line nodes of the current group. */ for(node=glh->list.head; node; node=next) { /* * Keep a record of the following node before we delete the current * node. */ next = node->next; /* * Discard this node? */ if(node->group == glh->group) _glh_discard_node(glh, node); }; /* * If there are any lines left, and we deleted any lines, there will * be gaps in the buffer. These need to be removed. */ if(glh->list.head) { int epos; /* The index of the last used element in the buffer */ /* * Find the line nearest the end of the buffer. */ GlLineNode *enode; for(node=glh->list.head, prev=NULL; node && node->start >= glh->list.head->start; prev=node, node = node->next) ; enode = prev; /* * Move the end line to abutt the end of the buffer, and remove gaps * between the lines that precede it. */ epos = glh->buflen; for(node=enode; node; node=node->prev) { int shift = epos - (node->start + node->nchar); if(shift) { memmove(glh->buffer + node->start + shift, glh->buffer + node->start, node->nchar); node->start += shift; }; epos = node->start; }; /* * Move the first line in the buffer to the start of the buffer, and * remove gaps between the lines that follow it. */ epos = 0; for(node=enode ? enode->next : NULL; node; node=node->next) { int shift = epos - node->start; if(shift) { memmove(glh->buffer + node->start + shift, glh->buffer + node->start, node->nchar); node->start += shift; }; epos = node->start + node->nchar; }; }; }; return; } /*....................................................................... * Temporarily enable or disable the history list. * * Input: * glh GlHistory * The input-line history maintenance object. * enable int If true, turn on the history mechanism. If * false, disable it. */ void _glh_toggle_history(GlHistory *glh, int enable) { if(glh) glh->enable = enable; } /*....................................................................... * Remove a given line location node from the history list, and return * it to the freelist. * * Input: * glh GlHistory * The input-line history maintenance object. * node GlLineNode * The node to be removed. This must be currently * in the list who's head is glh->list.head, or * be NULL. */ static void _glh_discard_node(GlHistory *glh, GlLineNode *node) { if(node) { /* * Make the node that precedes the node being removed point * to the one that follows it. */ if(node->prev) node->prev->next = node->next; else glh->list.head = node->next; /* * Make the node that follows the node being removed point * to the one that precedes it. */ if(node->next) node->next->prev = node->prev; else glh->list.tail = node->prev; /* * If we are deleting the node that is marked as the start point of the * last ID search, remove the cached starting point. */ if(node == glh->id_node) glh->id_node = NULL; /* * Return the node to the free list. */ node = (GlLineNode *) _del_FreeListNode(glh->list.node_mem, node); /* * Decrement the count of the number of lines in the buffer. */ glh->nline--; }; } /*....................................................................... * Lookup the details of a given history line, given its id. * * Input: * glh GlHistory * The input-line history maintenance object. * id GlLineID The sequential number of the line. * Input/Output: * line const char ** A pointer to the history line will be assigned * to *line. * group unsigned * The group membership of the line will be assigned * to *group. * timestamp time_t * The timestamp of the line will be assigned to * *timestamp. * Output: * return int 0 - The requested line wasn't found. * 1 - The line was found. */ int _glh_lookup_history(GlHistory *glh, GlhLineID id, const char **line, unsigned *group, time_t *timestamp) { GlLineNode *node; /* The located line location node */ /* * Check the arguments. */ if(!glh) return 0; /* * Search for the line that has the specified ID. */ node = _glh_find_id(glh, (GlhLineID) id); /* * Not found? */ if(!node) return 0; /* * Return the details of the line. */ if(line) *line = glh->buffer + node->start; if(group) *group = node->group; if(timestamp) *timestamp = node->timestamp; return 1; } /*....................................................................... * Lookup a node in the history list by its ID. * * Input: * glh GlHistory * The input-line history maintenance object. * id GlhLineID The ID of the line to be returned. * Output: * return GlLIneNode * The located node, or NULL if not found. */ static GlLineNode *_glh_find_id(GlHistory *glh, GlhLineID id) { GlLineNode *node; /* The node being checked */ /* * Is history enabled? */ if(!glh->enable || !glh->list.head) return NULL; /* * If possible, start at the end point of the last ID search. * Otherwise start from the head of the list. */ node = glh->id_node; if(!node) node = glh->list.head; /* * Search forwards from 'node'? */ if(node->id < id) { while(node && node->id != id) node = node->next; glh->id_node = node ? node : glh->list.tail; /* * Search backwards from 'node'? */ } else { while(node && node->id != id) node = node->prev; glh->id_node = node ? node : glh->list.head; }; /* * Return the located node (this will be NULL if the ID wasn't found). */ return node; } /*....................................................................... * Query the state of the history list. Note that any of the input/output * pointers can be specified as NULL. * * Input: * glh GlHistory * The input-line history maintenance object. * Input/Output: * enabled int * If history is enabled, *enabled will be * set to 1. Otherwise it will be assigned 0. * group unsigned * The current history group ID will be assigned * to *group. * max_lines int * The currently requested limit on the number * of history lines in the list, or -1 if * unlimited. */ void _glh_state_of_history(GlHistory *glh, int *enabled, unsigned *group, int *max_lines) { if(glh) { if(enabled) *enabled = glh->enable; if(group) *group = glh->group; if(max_lines) *max_lines = glh->max_lines; }; } /*....................................................................... * Get the range of lines in the history buffer. * * Input: * glh GlHistory * The input-line history maintenance object. * Input/Output: * oldest unsigned long * The sequential entry number of the oldest * line in the history list will be assigned * to *oldest, unless there are no lines, in * which case 0 will be assigned. * newest unsigned long * The sequential entry number of the newest * line in the history list will be assigned * to *newest, unless there are no lines, in * which case 0 will be assigned. * nlines int * The number of lines currently in the history * list. */ void _glh_range_of_history(GlHistory *glh, unsigned long *oldest, unsigned long *newest, int *nlines) { if(glh) { if(oldest) *oldest = glh->list.head ? glh->list.head->id : 0; if(newest) *newest = glh->list.tail ? glh->list.tail->id : 0; if(nlines) *nlines = glh->nline; }; } /*....................................................................... * Return the size of the history buffer and the amount of the * buffer that is currently in use. * * Input: * glh GlHistory * The input-line history maintenance object. * Input/Output: * buff_size size_t * The size of the history buffer (bytes). * buff_used size_t * The amount of the history buffer that * is currently occupied (bytes). */ void _glh_size_of_history(GlHistory *glh, size_t *buff_size, size_t *buff_used) { if(glh) { if(buff_size) *buff_size = glh->buflen; /* * Determine the amount of buffer space that is currently occupied. */ if(buff_used) { size_t used = 0; GlLineNode *node; for(node=glh->list.head; node; node=node->next) used += node->nchar; *buff_used = used; }; }; }