merge 64-bit fixes from Fernando Lucas Rodriguez <fernando_lr@terra.es>
[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     int32 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   uint32        pktime, res;
221
222   pktime = cart->records[0].timestamp;
223 #ifdef STORAGE_DEBUG
224   time_t pktime_val = pktime;
225   fprintf(stderr, "SavePacket for %u: %s", pktime, asctime(gmtime(&pktime_val)));
226 #endif
227
228   /* Generate the file name corresponding to the date of
229      the first record in the packet.  Since we know the
230      date, there's no need to use next_filename. */
231
232   res = next_poss_fn(datafile, pktime, cart->hdr.eggid);
233
234   /* Open the file for appending. */
235
236   Unprotect(datafile);
237   fp = fopen(datafile, "a");
238
239   /* If we can't open the file, the odds are it's because
240      the file is in a directory we've yet to create. */
241
242   if (fp == NULL) {
243 #ifdef STORAGE_DEBUG
244     fprintf(stderr, "SavePacket: no such file %s\n", datafile);
245 #endif
246     sp = strrchr(datafile, '/');
247     while (sp) {
248       strncpy(datatmp, datafile, sp - datafile);
249       datatmp[sp - datafile] = 0;
250       mkdir(datatmp, 0777);
251 #ifdef STORAGE_DEBUG
252       fprintf(stderr, "SavePacket: mkdir %s\n", datatmp);
253 #endif
254       sp = strchr(sp + 1, '/');
255     }
256
257     /* Now try re-opening the file.  */
258
259     Unprotect(datafile);
260     if ((fp = fopen(datafile, "a")) == NULL) {
261       fprintf(stderr, "SavePacket: cannot create database file %s.\n", datafile);
262       exit(-1);
263     }
264   }
265   
266   if (!fp) {
267 #ifdef STORAGE_DEBUG
268     fprintf(stderr, "SavePacket: error opening %s\n", datafile);
269 #endif
270     return -2;
271   }
272   packet = Packetize(cart);
273   if (!packet) return -1;
274   fwrite(packet, sizeof(char), cart->hdr.pktsize, fp);
275   free(packet);
276   fclose(fp);
277   Protect(datafile);
278   cart->hdr.numrec = 0;
279
280   return 1;
281 }
282
283 /* Open database for reading, positioning to specified location.
284    If eggid is less than zero, it will load any packet. */
285 int32 OpenDatabase(DBRec *dbp, uint32 tindex, int16 eggid) {
286   int32         res;
287
288 #ifdef STORAGE_DEBUG
289   time_t tindex_val = tindex;
290   fprintf(stderr, "OpenDatabase: Egg = %d, tindex = %u %s",
291     eggid, tindex, asctime(gmtime(&tindex_val)));
292 #endif
293   dbp->eggind = 0;
294   dbp->fp = NULL;
295   if ((res = next_filename(dbp->fn, tindex, eggid, &(dbp->eggind), TRUE)) < 0)
296     return res;
297     
298   Unprotect(dbp->fn);
299   dbp->fp = fopen(dbp->fn, "r");
300   if (dbp->fp == NULL) {
301     Protect(dbp->fn);
302 #ifdef STORAGE_DEBUG
303   fprintf(stderr, "OpenDatabase -- EOF\n");
304 #endif
305     return ERR_EOF;
306   } else {
307 #ifdef STORAGE_DEBUG
308   fprintf(stderr, "OpenDatabase -- Opened %s\n", dbp->fn);
309 #endif
310     return ERR_NONE;
311   }
312 }
313
314 /* Close database */
315 int32 CloseDatabase(DBRec *dbp) {
316   fclose(dbp->fp);
317 #ifdef STORAGE_DEBUG
318   fprintf(stderr, "CloseDatabase -- Closed %s\n", dbp->fn);
319 #endif
320   Protect(dbp->fn);
321   dbp->fp = NULL;
322   /* We preserve file name; this lets us not open the same
323      file name again unless we want to. */
324   return ERR_NONE;
325 }
326
327 /* Reset database allows us to open same file name again. */
328 int32 ResetDatabase(DBRec *dbp) {
329   *(dbp->fn) = 0;
330 #ifdef STORAGE_DEBUG
331   fprintf(stderr, "ResetDatabase\n");
332 #endif
333   return ERR_NONE;
334 }
335
336 /* Load next packet fitting specified time index and egg id.
337    If eggid is less than zero, it will load any packet. */
338
339 static int32 LoadNextPacket(DBRec *dbp, uint32 tindex, int16 eggid, EggCarton *cart) {
340   EggCarton     pktbuf;
341   int32         res, i;
342   uint32        findex = tindex, now;
343
344   now = getzulutime(NULL);
345   if (findex < PROJSTART) {
346     findex = PROJSTART;
347   }
348 #ifdef STORAGE_DEBUG
349   fprintf(stderr, "LoadNextPacket(%s, %u, %d)\n", dbp->fn, tindex, eggid);
350 #endif
351
352   /* See if the start address for this request is present
353      in the seek optimisation table.  If so, seek directly
354      to the address to avoid having to read through all
355      earlier packets in the database. */
356
357   i = seekOptIndex;
358   /* Conditioning while on tindex > 0 causes seek optimisation to be
359      skipped when a request for any packet is received. */
360   while (tindex > 0) { 
361       i--;
362       if (i < 0) {
363           i = SEEK_OPT_MAX - 1;
364       }
365       if (strcmp(seekOpt[i].filename, dbp->fn) == 0 &&
366           seekOpt[i].last_time == tindex) {
367           fseek(dbp->fp, seekOpt[i].next_packet, SEEK_SET);
368 #ifdef STORAGE_DEBUG
369         time_t last_time_val = seekOpt[i].last_time;
370         fprintf(stderr, "LoadNextPacket; Seek optimised [%d] to %d for\n    file %s at %u %s",
371               i,
372               seekOpt[i].next_packet, 
373               seekOpt[i].filename,
374               seekOpt[i].last_time,
375               asctime(gmtime(&last_time_val)));
376
377 #endif
378           break;
379       }
380       if (i == seekOptIndex) {
381 #ifdef STORAGE_DEBUG
382         time_t tindex_val = tindex;
383           fprintf(stderr, "LoadNextPacket; Cannot optimise seek in file %s\n    at %u %s",
384               dbp->fn, tindex, asctime(gmtime(&tindex_val)));
385 #endif
386           break;                      /* Search wrapped table--cannot optimise */
387       }
388   }
389   
390   while (1) {
391     res = FileLoadPacket(dbp->fp, &pktbuf);
392     if (res == ERR_EOF) {
393       /* File is finished.  Close and open the next.  We assume
394          that tindex and eggid have been tracking appropriately. */
395       CloseDatabase(dbp);
396
397       /* Advance findex to start of next day after the one we've
398          just exhausted. */
399
400 #define SECONDS_PER_DAY (24L * 60 * 60)
401       findex = ((findex / SECONDS_PER_DAY) + 1) * SECONDS_PER_DAY;
402 #ifdef STORAGE_DEBUG
403      time_t findex_val = findex;
404       fprintf(stderr, "LoadNextPacket; EOF, CloseDatabase, findex = %u: %s",
405           findex, asctime(gmtime(&findex_val)));
406 #endif
407       if (findex > now) {
408 #ifdef STORAGE_DEBUG
409      time_t now_val = now;
410           fprintf(stderr, "LoadNextPacket; EOF findex = %u > now = %u %s",
411           findex, now, asctime(gmtime(&now_val)));
412 #endif
413         return ERR_EOF;
414       }
415       /* Do not reset database; we won't open same one again. */
416       if ((res = OpenDatabase(dbp, findex, eggid)) < 0) return res;
417       continue;
418     }
419 /*
420 fprintf(stderr, "Eggid = %d Packet.eggid = %d\n", eggid, pktbuf.hdr.eggid);
421 */
422     if (eggid >= 0 && pktbuf.hdr.eggid != eggid) continue;
423     for (i = 0; i < pktbuf.hdr.numrec; i++) {
424 /*fprintf(stderr, " Rec %ld timestamp = %lu  tindex = %lu\n", i, pktbuf.records[i].timestamp, tindex);
425 */
426       if (pktbuf.records[i].timestamp > tindex) {
427         memcpy(cart, &pktbuf, sizeof(EggCarton));
428
429         /* Save the location of the packet following this one
430            in the seek optimisation table. */
431
432         strcpy(seekOpt[seekOptIndex].filename, dbp->fn);
433         seekOpt[seekOptIndex].last_time = pktbuf.records[pktbuf.hdr.numrec - 1].timestamp;
434         seekOpt[seekOptIndex].next_packet = ftell(dbp->fp);
435 #ifdef STORAGE_DEBUG
436      time_t last_time_val = seekOpt[seekOptIndex].last_time;
437         fprintf(stderr, "LoadNextPacket; Seek optimisation[%d] for %s\n    to address %d for time %u %s",
438             seekOptIndex,
439             seekOpt[seekOptIndex].filename,
440             seekOpt[seekOptIndex].next_packet, 
441             seekOpt[seekOptIndex].last_time,
442             asctime(gmtime(&last_time_val)));
443 #endif
444         seekOptIndex = (seekOptIndex + 1) % SEEK_OPT_MAX;
445         return ERR_NONE;
446       }
447     }
448   }
449 }
450
451 /* Load next packet following a particular time index and egg id */
452
453 int32 LoadPacket(uint32 tindex, int16 eggid, EggCarton *cart) {
454   DBRec db;
455   int32 res;
456
457   ResetDatabase(&db);   /* Here we need to be able to open same db as
458                            last time. */
459   if ((res = OpenDatabase(&db, tindex, eggid)) < 0) return res;
460   if ((res = LoadNextPacket(&db, tindex, eggid, cart)) < 0) return res;
461   return CloseDatabase(&db);
462 }
463
464 static int32 FileLoadPacket(FILE *fp, EggCarton *cart) {
465   char *rbuf;
466   int32 res;
467   uint16 pksize;
468
469   if (feof(fp)) return ERR_EOF;
470
471   if (fread(&(cart->hdr.type), sizeof(uint16), 1, fp) != 1) {
472     if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
473   }
474   if (fread(&(cart->hdr.pktsize), sizeof(uint16), 1, fp) != 1) {
475     if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
476   }
477
478   /* Since the file packet is in network byte order, we need to
479      be sure the length field is in host order before using
480      it to allocate and read the balance of the packet from the
481      file.  Once that's done, Unpacketize will take responsibility
482      for getting all the packet fields into host order. */
483   pksize = ntohs(cart->hdr.pktsize);
484
485   rbuf = (char *)malloc(pksize);
486   if (!rbuf) return ERR_NOMEM;
487
488   memcpy(rbuf, &(cart->hdr), 2*sizeof(uint16));
489   if (fread(rbuf+4, pksize - 4, 1, fp) != 1) return ERR_CNREAD;
490   
491   res = Unpacketize(cart, rbuf);
492   free(rbuf);
493
494   return res;
495 }
496
497 static int32 next_filename(char *fn, uint32 tindex, int16 eggid,
498                     int16 *lastind, int16 mustexist) {
499   struct stat   statbuf;
500   uint32        findex, now;
501   int32         res;
502   char          ldf[255];
503
504 #ifdef STORAGE_DEBUG
505   time_t tindex_val = tindex;
506   fprintf(stderr, "next_filename: egg = %d, mustexist = %d, tindex = %u %s",
507     eggid, mustexist, tindex, asctime(gmtime(&tindex_val)));
508 #endif
509   if (eggid < 0 && !mustexist) {
510 #ifdef STORAGE_DEBUG
511     fprintf(stderr, "next_filename: egg ID out of range.\n");
512 #endif
513     return ERR_INRANGE;
514   }
515
516   /* Can't go back before project began. */
517   now = getzulutime(NULL);
518   if (tindex < PROJSTART) {
519     findex = PROJSTART;
520   } else {
521     findex = tindex;
522   }
523
524   /* Search possible file names until one is found.  We jump by days,
525      assuming files don't have a greater granularity than that.  In
526      the existence-testing process, we are checking to make sure the
527      file name is different each time. */
528
529   if (lastind == NULL) eggid = eggtable[0].id;
530   else {
531     if (eggid < 0) eggid = eggtable[(*lastind)++].id;
532     if (*lastind == numeggs) *lastind = 0;
533   }
534
535   strcpy(ldf, fn);
536   do {
537     res = next_poss_fn(fn, findex, eggid);
538     findex += 86400L;
539     if (!strcmp(fn, ldf)) continue;
540     strcpy(ldf, fn);
541     /* Check for file.  If stat fails, reason is ENOENT, isn't it? */
542     if (stat(fn, &statbuf) >= 0) {
543 #ifdef STORAGE_DEBUG
544       fprintf(stderr, "next_filename: Found file %s\n", fn);
545 #endif
546         return ERR_NONE;
547     }
548     if (!mustexist) {
549 #ifdef STORAGE_DEBUG
550         fprintf(stderr, "next_filename: File %s does not exist.\n", fn);
551 #endif
552         return ERR_NOENT;
553     }
554   } while (findex < now+86400L);
555
556 #ifdef STORAGE_DEBUG
557       fprintf(stderr, "next_filename: End of file.\n");
558 #endif
559   return ERR_EOF;
560 }
561
562 /* tindex and eggid must be defined here */
563 static int32 next_poss_fn(char *fn, uint32 tindex, int16 eggid) {
564   char          datatmp[255], *sp;
565   int16         eggind;
566   struct tm     *tm;
567
568   time_t time_index = tindex;
569   tm = gmtime(&time_index);
570   strftime(datatmp, 255, datafmt, tm);
571 #ifdef STORAGE_DEBUG
572   fprintf(stderr, "next_poss_fn: egg = %d, tindex = %u %s",
573     eggid, tindex, asctime(tm));
574 #endif
575
576   /* If an %e phrase remains after processing by strftime
577      (it should be specified as %%e in the original
578      DATAFMT specification), interpolate the egg number
579      from this packet. */
580
581   if ((sp = strchr(datatmp, '%')) != NULL) {
582     if (sp[1] == 'e') {
583       for (eggind = 0;
584            eggind < numeggs && eggtable[eggind].id != eggid;
585            eggind++);
586       if (eggind == numeggs) {
587 #ifdef STORAGE_DEBUG
588         fprintf(stderr, "next_poss_fn: Egg number out of range.\n");
589 #endif
590         return ERR_INRANGE;
591       }
592       *sp = 0;
593       strcpy(fn, datatmp);
594       strcat(fn, eggtable[eggind].name);
595       strcat(fn, sp + 2);
596     } else {
597       fprintf(stderr, "Bad file format expr: %%%c\n", sp[1]);
598       strcpy(fn, datatmp);
599       return ERR_INRANGE;
600     }
601   } else {
602     strcpy(fn, datatmp);
603   }
604 #ifdef STORAGE_DEBUG
605   fprintf(stderr, "next_poss_fn: File name = %s\n", fn);
606 #endif
607   return ERR_NONE;
608 }