Imported Upstream version 5.1
[debian/gcpegg] / storage.c
1 /* PROGRAM:     eggsh
2  * FILE:        $Header: /home/egg/src/RCS/storage.c,v 1.4 1998/12/31 22:07:56 ghn Exp $
3  * PURPOSE:     Data storage functions
4  * AUTHOR:      Greg Nelson
5  * DATE:        98-05-09
6  *
7  * REVISED:
8  * $Log: storage.c,v $
9  * Revision 1.4  1998/12/31 22:07:56  ghn
10  * Rev 5 code: includes multi-reg support, HTML, etc.
11  *
12  * Revision 1.3  1998/08/03 20:32:46  kelvin
13  * File byte-order independence, STORAGE_DEBUG
14  *
15  * Revision 1.2  1998/08/01  17:18:45  ghn
16  * Fixes to prevent core dumps and make "database" engine work correctly.
17  *
18  * Revision 1.1  1998/07/21 11:36:21  ghn
19  * Initial revision
20  *
21  * Copyright 1998 - Greg Nelson
22  * Redistributable under the terms of the GNU Public Licence (GPL)
23  */
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <unistd.h>
28 #include <string.h>
29 #include <ctype.h>
30 #include <time.h>
31 #include <fcntl.h>
32 #include <sys/types.h>
33 #include <sys/time.h>
34 #include <sys/stat.h>
35 #include "global.h"
36 #include "genlib.h"
37 #include "storage.h"
38 #include "errnos.h"
39
40 /* Some systems (e.g. Linux) implement strdup but don't define
41    it by default in string.h.  Define it explicitly here, which
42    doesn't seem to do any harm on other systems.  If you encounter
43    an error on the following declaration, disable it for your
44    platform. */
45
46 extern char *strdup (const char *s1);
47
48 /* Eventually, these might deal with making a safe copy or mounting
49    and unmounting a partition. Until such time as we actually do
50    something with these, they are no-ops. */
51 #define Protect(x)
52 #define Unprotect(x)
53
54 #define DATAFMT         "%Y%m/%%e"
55 #define PROJSTART       901947600L
56
57 static char *datafmt;                 /* Data file format string */
58
59 /*  Prototypes for forward functions.  */
60
61 static int32 FileLoadPacket(FILE *fp, EggCarton *cart);
62 static int32 next_filename(char *fn, uint32 tindex, int16 eggid,
63                            int16 *lastind, int16 mustexist);
64 static int32 next_poss_fn(char *fn, uint32 tindex, int16 eggid);
65
66 /* The seek optimisation table saves the file name, last
67    packet returned, and corresponding file of the next
68    packet (if any) in the file, for the last SEEK_OPT_MAX
69    requests.  This allows LoadNextPacket to avoid searching
70    the entire database for the next packet if the request
71    matches one in the seek optimisation table.
72
73 */
74
75 #define SEEK_OPT_MAX MAX_BASKETS
76
77 struct seekopt {
78     char filename[256];
79     uint32 last_time;
80     long next_packet;
81 };
82
83 static struct seekopt seekOpt[SEEK_OPT_MAX];
84 static int seekOptIndex = 0;
85
86 /* The initialization provides a way to specify a format for the
87    database file names.  If the argument is null, it uses a default
88    from the environment, or it takes the default given above.  This
89    string is used as an argument to strftime(3) which replaces certain
90    characters with corresponding fields of the date and time.  This
91    provides an automatic way of generating a logical new filename on a
92    periodic basis.  Note that you can call InitStorage as many times
93    as you wish to change the format for file names opened
94    subsequently. */
95
96 int32 InitStorage(char *path) {
97   char *p, *np;
98   char ns[256], pa[12];
99   int changed = 0;
100
101   /* First, take this opportunity to clear the seek optimisation
102      table.  If we're changing the file name format, none of the
103      items in it will be valid in any case. */
104
105   memset(seekOpt, 0, sizeof seekOpt);
106
107   /* Set the path based on the environment variable, argument,
108      or default. */
109
110   if (path == NULL) {
111     if ((path = getenv("EGG_DATA")) == NULL) {
112       path = DATAFMT;
113     }
114   }
115
116   /* If the path contains any of our special "$" editing
117      codes, interpolate the requested text into the string.
118      Strftime "%" editing codes are left intact. */
119
120   ns[0] = 0;
121   p = path;
122   while ((np = strchr(p, '$')) != NULL) {
123     char *phrargs = np + 1;
124
125     /* Copy portion of string prior to phrase to output. */
126
127     if (np > p) {
128         int l = strlen(ns);
129
130         memcpy(ns + l, p, np - p);
131         ns[l + (np - p)] = 0;
132     }
133
134     /* Parse format phrase and possible arguments. */
135
136     while ((*phrargs != 0) && !isalpha(*phrargs)) {
137         phrargs++;
138     }
139     if (*phrargs == 0) {
140         fprintf(stderr, "Data format string error in:\n    %s\nunterminated $ phrase.\n", path);
141         exit(-1);
142     }
143
144     /* Copy arguments, if any, to second character of arguments
145        string for possible use by phrase interpreters in
146        sprintf. */
147
148     pa[0] = '%';                      /* Start editing phrase */
149     pa[1] = 0;
150     if (phrargs > (np + 1)) {
151         memcpy(pa + 1, np + 1, phrargs - (np + 1));
152         pa[1 + (phrargs - (np + 1))] = 0;
153     }
154 /*fprintf(stderr, "Phrase arguments = <%s>\n", pa);*/
155
156     /* Now interpret the specific format phrase letters.
157        The value selected by each phrase should be concatenated
158        to the output string ns.  Available format phrases
159        are:
160
161            $b          Basket name
162            $e          Egg name
163            $[0][n]E    Egg number, right justified,
164                        optionally zero filled in n characters
165
166     */
167
168
169     switch (*phrargs) {
170
171         case 'b':                     /* Basket name */
172             strcat(ns, baskettable[0].name);
173             break;
174
175         case 'e':                     /* Egg name */
176             strcat(ns, eggtable[0].name);
177             break;
178
179         case 'E':                     /* Egg number, edited decimal number */
180             strcat(pa, "d");
181             sprintf(ns + strlen(ns), pa, eggtable[0].id);
182             break;
183
184         default:
185             fprintf(stderr, "Data format string error in:\n    %s\nunknown phrase $%c.\n",
186                     path, *phrargs);
187             exit(-1);
188     }
189
190     /* Adjust pointer to resume scan following the phrase
191        in the source string. */
192
193     p = phrargs + 1;
194     changed++;
195   }
196
197   if (changed) {
198     strcat(ns, p);                    /* Concatenate balance of string */
199     path = strdup(ns);
200     if (path == NULL) {
201         fprintf(stderr, "Cannot allocate memory for file name format string.\n");
202         exit(-1);
203     }
204 /*printf("Expanded path name phrase = <%s>\n", path); exit(0);*/
205   }
206
207   datafmt = path;
208   return 0;
209 }
210
211 /* Save packet is the basic save function.  It does not take a
212    filename, relying on the datafmt variable created during
213    initialization to generate an appropriate filename.  The
214    date for the saved data is based on the timestamp of the packet's
215    first record. */
216
217 int32 SavePacket(EggCarton *cart) {
218   FILE          *fp;
219   char          *packet, datatmp[255], datafile[255], *sp;
220   int32         pktime, res;
221
222   pktime = cart->records[0].timestamp;
223 #ifdef STORAGE_DEBUG
224   fprintf(stderr, "SavePacket for %lu: %s", pktime, asctime(gmtime((time_t *) &pktime)));
225 #endif
226
227   /* Generate the file name corresponding to the date of
228      the first record in the packet.  Since we know the
229      date, there's no need to use next_filename. */
230
231   res = next_poss_fn(datafile, pktime, cart->hdr.eggid);
232
233   /* Open the file for appending. */
234
235   Unprotect(datafile);
236   fp = fopen(datafile, "a");
237
238   /* If we can't open the file, the odds are it's because
239      the file is in a directory we've yet to create. */
240
241   if (fp == NULL) {
242 #ifdef STORAGE_DEBUG
243     fprintf(stderr, "SavePacket: no such file %s\n", datafile);
244 #endif
245     sp = strrchr(datafile, '/');
246     while (sp) {
247       strncpy(datatmp, datafile, sp - datafile);
248       datatmp[sp - datafile] = 0;
249       mkdir(datatmp, 0777);
250 #ifdef STORAGE_DEBUG
251       fprintf(stderr, "SavePacket: mkdir %s\n", datatmp);
252 #endif
253       sp = strchr(sp + 1, '/');
254     }
255
256     /* Now try re-opening the file.  */
257
258     Unprotect(datafile);
259     if ((fp = fopen(datafile, "a")) == NULL) {
260       fprintf(stderr, "SavePacket: cannot create database file %s.\n", datafile);
261       exit(-1);
262     }
263   }
264   
265   if (!fp) {
266 #ifdef STORAGE_DEBUG
267     fprintf(stderr, "SavePacket: error opening %s\n", datafile);
268 #endif
269     return -2;
270   }
271   packet = Packetize(cart);
272   if (!packet) return -1;
273   fwrite(packet, sizeof(char), cart->hdr.pktsize, fp);
274   free(packet);
275   fclose(fp);
276   Protect(datafile);
277   cart->hdr.numrec = 0;
278
279   return 1;
280 }
281
282 /* Open database for reading, positioning to specified location.
283    If eggid is less than zero, it will load any packet. */
284 int32 OpenDatabase(DBRec *dbp, uint32 tindex, int16 eggid) {
285   int32         res;
286
287 #ifdef STORAGE_DEBUG
288   fprintf(stderr, "OpenDatabase: Egg = %d, tindex = %ld %s",
289     eggid, tindex, asctime(gmtime((time_t *) &tindex)));
290 #endif
291   dbp->eggind = 0;
292   dbp->fp = NULL;
293   if ((res = next_filename(dbp->fn, tindex, eggid, &(dbp->eggind), TRUE)) < 0)
294     return res;
295     
296   Unprotect(dbp->fn);
297   dbp->fp = fopen(dbp->fn, "r");
298   if (dbp->fp == NULL) {
299     Protect(dbp->fn);
300 #ifdef STORAGE_DEBUG
301   fprintf(stderr, "OpenDatabase -- EOF\n");
302 #endif
303     return ERR_EOF;
304   } else {
305 #ifdef STORAGE_DEBUG
306   fprintf(stderr, "OpenDatabase -- Opened %s\n", dbp->fn);
307 #endif
308     return ERR_NONE;
309   }
310 }
311
312 /* Close database */
313 int32 CloseDatabase(DBRec *dbp) {
314   fclose(dbp->fp);
315 #ifdef STORAGE_DEBUG
316   fprintf(stderr, "CloseDatabase -- Closed %s\n", dbp->fn);
317 #endif
318   Protect(dbp->fn);
319   dbp->fp = NULL;
320   /* We preserve file name; this lets us not open the same
321      file name again unless we want to. */
322   return ERR_NONE;
323 }
324
325 /* Reset database allows us to open same file name again. */
326 int32 ResetDatabase(DBRec *dbp) {
327   *(dbp->fn) = 0;
328 #ifdef STORAGE_DEBUG
329   fprintf(stderr, "ResetDatabase\n");
330 #endif
331   return ERR_NONE;
332 }
333
334 /* Load next packet fitting specified time index and egg id.
335    If eggid is less than zero, it will load any packet. */
336
337 static int32 LoadNextPacket(DBRec *dbp, uint32 tindex, int16 eggid, EggCarton *cart) {
338   EggCarton     pktbuf;
339   int32         res, i;
340   uint32        findex = tindex, now;
341
342   now = getzulutime(NULL);
343   if (findex < PROJSTART) {
344     findex = PROJSTART;
345   }
346 #ifdef STORAGE_DEBUG
347   fprintf(stderr, "LoadNextPacket(%s, %lu, %d)\n", dbp->fn, tindex, eggid);
348 #endif
349
350   /* See if the start address for this request is present
351      in the seek optimisation table.  If so, seek directly
352      to the address to avoid having to read through all
353      earlier packets in the database. */
354
355   i = seekOptIndex;
356   /* Conditioning while on tindex > 0 causes seek optimisation to be
357      skipped when a request for any packet is received. */
358   while (tindex > 0) { 
359       i--;
360       if (i < 0) {
361           i = SEEK_OPT_MAX - 1;
362       }
363       if (strcmp(seekOpt[i].filename, dbp->fn) == 0 &&
364           seekOpt[i].last_time == tindex) {
365           fseek(dbp->fp, seekOpt[i].next_packet, SEEK_SET);
366 #ifdef STORAGE_DEBUG
367           fprintf(stderr, "LoadNextPacket; Seek optimised [%ld] to %ld for\n    file %s at %lu %s",
368               i,
369               seekOpt[i].next_packet, 
370               seekOpt[i].filename,
371               seekOpt[i].last_time,
372               asctime(gmtime((time_t *) &(seekOpt[i].last_time))));
373
374 #endif
375           break;
376       }
377       if (i == seekOptIndex) {
378 #ifdef STORAGE_DEBUG
379           fprintf(stderr, "LoadNextPacket; Cannot optimise seek in file %s\n    at %lu %s",
380               dbp->fn, tindex, asctime(gmtime((time_t *) &(tindex))));
381 #endif
382           break;                      /* Search wrapped table--cannot optimise */
383       }
384   }
385   
386   while (1) {
387     res = FileLoadPacket(dbp->fp, &pktbuf);
388     if (res == ERR_EOF) {
389       /* File is finished.  Close and open the next.  We assume
390          that tindex and eggid have been tracking appropriately. */
391       CloseDatabase(dbp);
392
393       /* Advance findex to start of next day after the one we've
394          just exhausted. */
395
396 #define SECONDS_PER_DAY (24L * 60 * 60)
397       findex = ((findex / SECONDS_PER_DAY) + 1) * SECONDS_PER_DAY;
398 #ifdef STORAGE_DEBUG
399       fprintf(stderr, "LoadNextPacket; EOF, CloseDatabase, findex = %lu: %s",
400           findex, asctime(gmtime((time_t *) &findex)));
401 #endif
402       if (findex > now) {
403 #ifdef STORAGE_DEBUG
404           fprintf(stderr, "LoadNextPacket; EOF findex = %lu > now = %lu %s",
405           findex, now, asctime(gmtime((time_t *) &now)));
406 #endif
407         return ERR_EOF;
408       }
409       /* Do not reset database; we won't open same one again. */
410       if ((res = OpenDatabase(dbp, findex, eggid)) < 0) return res;
411       continue;
412     }
413 /*
414 fprintf(stderr, "Eggid = %d Packet.eggid = %d\n", eggid, pktbuf.hdr.eggid);
415 */
416     if (eggid >= 0 && pktbuf.hdr.eggid != eggid) continue;
417     for (i = 0; i < pktbuf.hdr.numrec; i++) {
418 /*fprintf(stderr, " Rec %ld timestamp = %lu  tindex = %lu\n", i, pktbuf.records[i].timestamp, tindex);
419 */
420       if (pktbuf.records[i].timestamp > tindex) {
421         memcpy(cart, &pktbuf, sizeof(EggCarton));
422
423         /* Save the location of the packet following this one
424            in the seek optimisation table. */
425
426         strcpy(seekOpt[seekOptIndex].filename, dbp->fn);
427         seekOpt[seekOptIndex].last_time = pktbuf.records[pktbuf.hdr.numrec - 1].timestamp;
428         seekOpt[seekOptIndex].next_packet = ftell(dbp->fp);
429 #ifdef STORAGE_DEBUG
430         fprintf(stderr, "LoadNextPacket; Seek optimisation[%d] for %s\n    to address %ld for time %lu %s",
431             seekOptIndex,
432             seekOpt[seekOptIndex].filename,
433             seekOpt[seekOptIndex].next_packet, 
434             seekOpt[seekOptIndex].last_time,
435             asctime(gmtime((time_t *) &(seekOpt[seekOptIndex].last_time))));
436 #endif
437         seekOptIndex = (seekOptIndex + 1) % SEEK_OPT_MAX;
438         return ERR_NONE;
439       }
440     }
441   }
442 }
443
444 /* Load next packet following a particular time index and egg id */
445
446 int32 LoadPacket(uint32 tindex, int16 eggid, EggCarton *cart) {
447   DBRec db;
448   int32 res;
449
450   ResetDatabase(&db);   /* Here we need to be able to open same db as
451                            last time. */
452   if ((res = OpenDatabase(&db, tindex, eggid)) < 0) return res;
453   if ((res = LoadNextPacket(&db, tindex, eggid, cart)) < 0) return res;
454   return CloseDatabase(&db);
455 }
456
457 static int32 FileLoadPacket(FILE *fp, EggCarton *cart) {
458   char *rbuf;
459   int32 res;
460   uint16 pksize;
461
462   if (feof(fp)) return ERR_EOF;
463
464   if (fread(&(cart->hdr.type), sizeof(uint16), 1, fp) != 1) {
465     if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
466   }
467   if (fread(&(cart->hdr.pktsize), sizeof(uint16), 1, fp) != 1) {
468     if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
469   }
470
471   /* Since the file packet is in network byte order, we need to
472      be sure the length field is in host order before using
473      it to allocate and read the balance of the packet from the
474      file.  Once that's done, Unpacketize will take responsibility
475      for getting all the packet fields into host order. */
476   pksize = ntohs(cart->hdr.pktsize);
477
478   rbuf = (char *)malloc(pksize);
479   if (!rbuf) return ERR_NOMEM;
480
481   memcpy(rbuf, &(cart->hdr), 2*sizeof(uint16));
482   if (fread(rbuf+4, pksize - 4, 1, fp) != 1) return ERR_CNREAD;
483   
484   res = Unpacketize(cart, rbuf);
485   free(rbuf);
486
487   return res;
488 }
489
490 static int32 next_filename(char *fn, uint32 tindex, int16 eggid,
491                     int16 *lastind, int16 mustexist) {
492   struct stat   statbuf;
493   uint32        findex, now;
494   int32         res;
495   char          ldf[255];
496
497 #ifdef STORAGE_DEBUG
498   fprintf(stderr, "next_filename: egg = %d, mustexist = %d, tindex = %lu %s",
499     eggid, mustexist, tindex, asctime(gmtime((time_t *) &tindex)));
500 #endif
501   if (eggid < 0 && !mustexist) {
502 #ifdef STORAGE_DEBUG
503     fprintf(stderr, "next_filename: egg ID out of range.\n");
504 #endif
505     return ERR_INRANGE;
506   }
507
508   /* Can't go back before project began. */
509   now = getzulutime(NULL);
510   if (tindex < PROJSTART) {
511     findex = PROJSTART;
512   } else {
513     findex = tindex;
514   }
515
516   /* Search possible file names until one is found.  We jump by days,
517      assuming files don't have a greater granularity than that.  In
518      the existence-testing process, we are checking to make sure the
519      file name is different each time. */
520
521   if (lastind == NULL) eggid = eggtable[0].id;
522   else {
523     if (eggid < 0) eggid = eggtable[(*lastind)++].id;
524     if (*lastind == numeggs) *lastind = 0;
525   }
526
527   strcpy(ldf, fn);
528   do {
529     res = next_poss_fn(fn, findex, eggid);
530     findex += 86400L;
531     if (!strcmp(fn, ldf)) continue;
532     strcpy(ldf, fn);
533     /* Check for file.  If stat fails, reason is ENOENT, isn't it? */
534     if (stat(fn, &statbuf) >= 0) {
535 #ifdef STORAGE_DEBUG
536       fprintf(stderr, "next_filename: Found file %s\n", fn);
537 #endif
538         return ERR_NONE;
539     }
540     if (!mustexist) {
541 #ifdef STORAGE_DEBUG
542         fprintf(stderr, "next_filename: File %s does not exist.\n", fn);
543 #endif
544         return ERR_NOENT;
545     }
546   } while (findex < now+86400L);
547
548 #ifdef STORAGE_DEBUG
549       fprintf(stderr, "next_filename: End of file.\n");
550 #endif
551   return ERR_EOF;
552 }
553
554 /* tindex and eggid must be defined here */
555 static int32 next_poss_fn(char *fn, uint32 tindex, int16 eggid) {
556   char          datatmp[255], *sp;
557   int16         eggind;
558   struct tm     *tm;
559
560   tm = gmtime((time_t *) &tindex);
561   strftime(datatmp, 255, datafmt, tm);
562 #ifdef STORAGE_DEBUG
563   fprintf(stderr, "next_poss_fn: egg = %d, tindex = %lu %s",
564     eggid, tindex, asctime(tm));
565 #endif
566
567   /* If an %e phrase remains after processing by strftime
568      (it should be specified as %%e in the original
569      DATAFMT specification), interpolate the egg number
570      from this packet. */
571
572   if ((sp = strchr(datatmp, '%')) != NULL) {
573     if (sp[1] == 'e') {
574       for (eggind = 0;
575            eggind < numeggs && eggtable[eggind].id != eggid;
576            eggind++);
577       if (eggind == numeggs) {
578 #ifdef STORAGE_DEBUG
579         fprintf(stderr, "next_poss_fn: Egg number out of range.\n");
580 #endif
581         return ERR_INRANGE;
582       }
583       *sp = 0;
584       strcpy(fn, datatmp);
585       strcat(fn, eggtable[eggind].name);
586       strcat(fn, sp + 2);
587     } else {
588       fprintf(stderr, "Bad file format expr: %%%c\n", sp[1]);
589       strcpy(fn, datatmp);
590       return ERR_INRANGE;
591     }
592   } else {
593     strcpy(fn, datatmp);
594   }
595 #ifdef STORAGE_DEBUG
596   fprintf(stderr, "next_poss_fn: File name = %s\n", fn);
597 #endif
598   return ERR_NONE;
599 }