chiark / gitweb /
much work on hash tables etc.
[chiark-tcl.git] / cdb / writeable.c
1 /**/
2
3 #include <nettle/md4.h>
4
5 #include "chiark_tcl_cdb.h"
6
7
8
9 struct ht_forall_ctx;
10
11
12
13 static void maybe_close(int fd) {
14   if (fd>=0) close(fd);
15 }
16
17 #define PE do{                                                  \
18     rc= cht_posixerr(ip, errno, "failed to " PE); goto x_rc;    \
19   }while(0)
20
21 /*---------- Pathbuf ----------*/
22
23 typedef struct {
24   char *buf, *sfx;
25 } Pathbuf;
26
27 #define MAX_SUFFIX 4
28
29 static void pathbuf_init(Pathbuf *pb, const char *pathb) {
30   int l= strlen(pathb);
31   pb->buf= TALLOC(l + 4);
32   memcpy(pb->buf, pathb, l);
33   pb->sfx= pb->buf + l;
34   *pb->sfx++= '.';
35 }
36 static const char *pathbuf_sfx(Pathbuf *pb, const char *suffix) {
37   assert(strlen(suffix) <= MAX_SUFFIX);
38   strcpy(pb->sfx, suffix);
39   return pb->buf;
40 }
41 static void pathbuf_free(Pathbuf *pb) {
42   TFREE(pb->buf);
43   pb->buf= 0;
44 }
45
46 /*---------- Our hash table ----------*/
47
48 typedef struct HashTable {
49   Tcl_HashTable t;
50   Byte confound[16];
51 } HashTable;
52
53 typedef struct HashDatum {
54   int len;
55   Byte data[1];
56 } HashDatum;
57
58 static unsigned int hkt_hash(Tcl_HashTable *tht, void *key_v) {
59   HashTable *ht= (void*)tht;
60   const HashDatum *key= key_v;
61
62   struct md4_ctx mdc;
63   Byte mdr[MD4_DIGEST_SIZE];
64   
65   md4_init(&mdc);
66   md4_update(&mdc, sizeof(ht->confound), ht->confound);
67   md4_update(&mdc, key->len, key->data);
68   md4_digest(&mdc, sizeof(mdr), mdr);
69   assert(sizeof(int) <= MD4_DIGEST_SIZE);
70   return *(int*)md4;
71 }
72
73 static const struct Tcl_HashKeyType ht_keytype= {
74   TCL_HASH_KEY_TYPE_VERSION, 0,
75   htk_hash, htk_compare, 0, 0
76 }
77
78 static HashDatum *htd_prep(int len) {
79   HashDatum *hd;
80   hd= TALLOC(offsetof(HashDatum, data) + len);
81   hd->len= len;
82 }  
83 static Byte *htd_fillptr(HashDatum *hd) {
84   return hd->data;
85 }
86 static void htd_fill(HashDatum *hd, const Byte *data) {
87   memcpy(hd->data, data, hd->len);
88 }
89
90 static int ht_setup(HashTable *ht) {
91   rc= cht_get_urandom(ip, ht->confound, sizeof(ht->confound));
92   if (rc) return rc;
93   
94   Tcl_InitCustomHashTable(&ht->t, TCL_CUSTOM_PTR_KEYS, &ht_keytype);
95   return TCL_OK;
96 }
97 static void ht_update(HashTable *ht, HashDatum *key_eat, HashDatum *val_eat) {
98   Tcl_HashEntry *he;
99   int new;
100
101   he= Tcl_CreateHashEntry(&ht->t, key_eat, &new);
102     /* eats the key! (since the data structure owns the memory) */
103   if (!new) TFREE(Tcl_GetHashValue(he));
104   Tcl_SetHashValue(he, val_eat);
105 }
106
107 static int ht_forall(HashTable *ht,
108                      int (*fn)(const HashDatum *key, const HashDatum *val,
109                                struct ht_forall_ctx *ctx),
110                      struct ht_forall_ctx *ctx) {
111   /* Returns first nonzero value returned by any call to fn, or 0. */
112   Tcl_HashSearch sp;
113   Tcl_HashEntry *he;
114   HashDatum *key, *val;
115   
116   for (he= Tcl_FirstHashEntry(&ht->t, &sp);
117        he;
118        he= Tcl_NextHashEntry(&ht->t, &sp)) {
119     val= Tcl_GetHashValue(he);
120     if (!val->len) continue;
121
122     key= Tcl_GetHashKey(&ht->t, he);
123     
124     r= fn(key, val, ctx);
125     if (r) return r;
126   }
127   return 0;
128 }
129
130 /*---------- Rw data structure ----------*/
131
132 typedef struct {
133   int ix, autocompact;
134   int cdb_fd, lock_fd;
135   struct cdb cdb; /* valid iff cdb_fd >= 0 */
136   off_t cdb_bytes; /* valid iff cdb_fd >= 0 */
137   FILE *logfile;
138   HashTable logincore;
139   Pathbuf pbsome, pbother;
140   off_t mainsz;
141 } Rw;
142
143 static void destroy_cdbrw_idtabcb(Tcl_Interp *ip, void *val) { abort(); }
144
145 int rw_close(Interp *ip, Rw *rw) {
146   int rc, r;
147
148   rc= TCL_OK;
149   ht_destroy(rw);
150   maybe_close(rw->cdb_fd);
151   maybe_close(rw->lock_fd);
152
153   if (rw->logfile) {
154     r= fclose(rw->logfile);
155     if (r && ip) { rc= cht_posixerr(ip, errno, "data loss! failed to"
156                                     " fclose logfile during untidy close"); }
157   }
158
159   pathbuf_free(&rw->pbsome); pathbuf_free(&rw->pbother);
160   TFREE(rw);
161   return rc;
162 };
163 void destroy_cdbrw_idtabcb(Interp *ip, Rw *rw) { rw_close(0,rw); }
164
165 const IdDataSpec cdbtcl_rwdatabases= {
166   "cdb-rwdb", "cdb-openrwdatabases-table", destroy_cdbrw_idtabcb
167 };
168
169 /*---------- File handling ----------*/
170
171 int acquire_lock(Tcl_Interp *ip, PathBuf *pb, int *lockfd_r) {
172   /* *lockfd_r must be -1 on entry.  If may be set to >=0 even
173    * on error, and must be closed by the caller. */
174   mode_t umask, lockmode;
175   struct flock fl;
176
177   umask= umask(~(mode_t)0);
178   umask(umask);
179
180   lockmode= 0666 & ~((umask & 0444)>>1);
181   /* Remove r where umask would remove w;
182    * eg umask intending 0664 here gives 0660 */
183   
184   *lockfd_r= open(pathbuf_sfx(".lock"), O_RDONLY|O_CREAT, lockmode);
185   if (*lockfd_r < 0)
186     return cht_posixerr(ip, errno, "could not open/create lockfile");
187
188   fl.l_type= F_WRLCK;
189   fl.l_whence= SEEK_SET;
190   fl.l_start= 0;
191   fl.l_end= 0;
192   fl.l_pid= getpid();
193
194   r= fcntl(*lockfd_r, F_SETLK, &fl);
195   if (r == -1) {
196     if (errno == EACCES || errno == EAGAIN)
197       return cht_staticerr(ip, "lock held by another process", "CDB LOCKED");
198     else return cht_posixerr(ip, errno, "unexpected error from fcntl while"
199                              " acquiring lock");
200   }
201 }
202
203 /*---------- Log reading ----------*/
204
205 static int readlognum(FILE *f, int delim, int *num_r) {
206   int c;
207   char numbuf[20], *p, *ep;
208   unsigned long ul;
209
210   p= numbuf;
211   for (;;) {
212     c= getc(f);  if (c==EOF) return -2;
213     if (c == delim) break;
214     if (!isdigit((unsigned char)c)) return -2;
215     *p++= c;
216     if (p == numbuf+sizeof(numbuf)) return -2;
217   }
218   if (p == numbuf) return -2;
219   *p= 0;
220
221   errno=0; ul= strtoul(numbuf, &ep, 10);
222   if (*ep || errno || ul >= INT_MAX/2) return -2;
223   *num_r= ul;
224   return 0;
225 }
226
227 static int readstorelogrecord(FILE *f, HashTable *ht,
228                               void (*updatefn)(HashTable*, HashDatum*,
229                                                HashDatum*)) {
230   /* returns:
231    *     0 for OK
232    *     -1 eof
233    *     -2 corrupt or error
234    *     -3 got newline indicating end
235    */
236   int keylen, vallen;
237   HashDatum *key, *val;
238
239   c= getc(f);
240   if (c==EOF) { if (feof(f)) return -1; return -2; }
241   if (c=='\n') return -3;
242   if (c!='+') return -2;
243
244   rc= readlognum(f, ',', &keylen);  if (rc) return rc;
245   rc= readlognum(f, ':', &vallen);  if (rc) return rc;
246
247   key= htd_prep(keylen);
248   val= htd_prep(vallen);
249
250   r= fread(htd_fillptr(key), 1,keylen, f);
251   if (r!=keylen) goto x2_free_keyval;
252
253   c= getc(f);  if (c!='-') goto x2_free_keyval;
254   c= getc(f);  if (c!='>') goto x2_free_keyval;
255   
256   r= fread(htd_fillptr(val), 1,vallen, f);
257   if (r!=vallen) goto x2_free_keyval;
258
259   updatefn(ht, key, val);
260   return TCL_OK;
261
262 x2_free_keyval;
263   TFREE(val);
264   TFREE(key);
265   return -2;
266 }
267
268 /*---------- Creating ----------*/
269
270 int cht_do_cdbwr_create_empty(ClientData cd, Tcl_Interp *ip,
271                               const char *pathb) {
272   static const char *const toremoves[]= {
273     ".main", ".cdb", ".log", ".tmp", 0
274   }
275
276   Pathbuf pb;
277   int lock_fd=-1, fd=-1, rc;
278   const char *const *toremove;
279
280   pathbuf_init(&pb, pathb);
281   rc= acquire_lock(ip, &pb, &lock_fd);  if (rc) goto x_rc;
282   
283   fd= open(pathbuf_sfx(".main"), O_RDWR|O_CREAT|O_EXCL, 0666);
284   if (fd <= 0) PE("create new database file");
285
286   for (toremoves=toremove; *toremove; toremove++) {
287     r= remove(*toremove);
288     if (r && errno != ENOENT)
289       PE("delete possible spurious file during creation");
290   }
291   
292   rc= TCL_OK;
293
294  x_rc:
295   maybe_close(fd);
296   maybe_close(lock_fd);
297   pathbuf_free(&pb);
298   return rc;
299 }
300
301 /*---------- Opening ----------*/
302
303 int cht_do_cdbwr_open(ClientData cd, Tcl_Interp *ip,
304                       const char *pathb, Tcl_Obj *on_info, void **result) {
305   const Cdbwr_SubCommand *subcmd= cd;
306   int rc, mainfd=-1;
307   Rw *rw;
308   struct stat stab;
309   fpos_t logrecstart;
310
311   rw= TALLOC(sizeof(*rw));
312   rc= ht_setup(&rw->logincore);  if (rc) { TFREE(rw); return rc; }
313   rw->cdb_fd= rw->lock_fd= -1;  rw->logfile= 0;
314   pathbuf_init(&rw->pbsome, &pathb);
315   pathbuf_init(&rw->pbother, &pathb);
316   rw->autocompact= 1;
317
318   mainfd= open(pathbuf_sfx(&rw->pbsome,".main"), O_RDONLY);
319   if (mainfd<0) PE("open exist3ing database file .main");
320   rc= acquire_lock(ip, &rw->pbsome, &rw->lock_fd);  if (rc) goto x_rc;
321
322   r= stat(mainfd, &stab);  if (r) PE("fstat .main");
323   rw->mainsz= stab.st_size;
324
325   rw->cdb_fd= open(pathbuf_sfx(&rw->pbsome,".cdb"), O_RDONLY);
326   if (fd>=0) {
327     r= cdb_init(&rw->cdb, rw->cdb_fd);
328     if (r) {
329       rc= cht_posixerr(ip, errno, "failed to initialise cdb reader");
330       close(rw->cdb_fd);  rw->cdb_fd= -1;  goto x_rc;
331     }
332   } else if (errno == ENOENT) {
333     if (rw->mainsz) {
334       rc= cht_staticerr(ip, ".cdb does not exist but .main is nonempty -"
335                         " .cdb must have been accidentally deleted!",
336                         "CDB CDBMISSING");
337       goto x_rc;
338     }
339     /* fine */
340   } else {
341     PE("open .cdb");
342   }
343
344   rw->logfile= fopen(pathbuf_sfx(&rw->pbsome,".log"), "r+");
345   if (!rw->logfile) {
346     if (errno != ENOENT) PE("failed to open .log during open");
347     rw->logfile= fopen(rw->pbsome.buf, "w");
348     if (!rw->logfile) PE("create .log during (clean) open");
349   } else { /* rw->logfile */
350     r= fstat(fileno(rw->logfile), &stab);
351     if (r==-1) PE("fstat .log during open");
352     rc= infocb(rw, "open-dirty-start", "log=%luby",
353                (unsigned long)stab.st_size);
354     if (rc) goto x_rc;
355
356     for (;;) {
357       r= fgetpos(rw->logfile, &logrecstart);
358       if (r) PE("fgetpos .log during (dirty) open");
359       r= readstorelogrecord(rw->logfile, &rw->logincore, ht_update);
360       if (ferror(rw->logfile)) {
361         rc= cht_posixerr(ip, errno, "error reading .log during (dirty) open");
362         goto x_rc;
363       }
364       if (r==-1) {
365         break;
366       } else if (r==-2 || r==-3) {
367         char buf[100];
368         r= fgetpos(rw->logfile, &logjunkpos);
369         if (r) PE("fgetpos .log during report of junk in dirty open");
370
371         snprintf(buf,sizeof(buf), "CDB SYNTAX LOG %lu %lu",
372                  (unsigned long)junkpos, (unsigned long)logrecstart);
373
374         if (!(subcmd->flags & RWSCF_OKJUNK)) {
375           Tcl_SetObjErrorCode(ip, Tcl_NewStringObj(buf,-1));
376           snprintf(buf,sizeof(buf),"%lu",(unsigned long)junkpos);
377           Tcl_ResetResult(ip);
378           Tcl_AppendResult(ip, "syntax error (junk) in .log during"
379                            " (dirty) open, at file position ", buf, (char*)0);
380           rc= TCL_ERROR;
381           goto x_rc;
382         }
383         rc= infocb(rw, "open-dirty-junk", "errorfpos=%luby {%s}",
384                    (unsigned long)logjunkpos, buf);
385         if (rc) goto x_rc;
386
387         r= fsetpos(rw->logfile, logrecstart);
388         if (r) PE("failed to fsetpos .log before junk during dirty open");
389
390         r= ftruncate(fileno(rw->logfile), logrecstart);
391         if (r) PE("ftruncate .log to chop junk during dirty open");
392       } else {
393         assert(!r);
394       }
395     }
396   }
397   /* now log is positioned for appending and everything is read */
398
399   *result= rw;
400   maybe_close(mainfd);
401   return TCL_OK;
402
403  x_rc:
404   rw_close(0,rw);
405   maybe_close(mainfd);
406   return rc;
407 }
408
409 /*---------- Compacting ----------*/
410
411 static int compact_core(Tcl_Interp *ip, Rw *rw, unsigned long logsz) {
412   /* creates new .cdb and .main
413    * closes logfile
414    * leaves .log with old data
415    * leaves cdb fd open onto old db
416    * leaves logincore full of crap
417    */
418   int rc;
419   int cdbfd, cdbmaking;
420   struct ht_forall_ctx {
421     struct cdb_make cdbm;
422     FILE *mainfile;
423     int count;
424   } addctx;
425
426   a.mainfile= 0;
427   cdbfd= -1;
428   cdbmaking= 0;
429
430   r= fclose(rw->logfile);
431   if (r) { rc= cht_posixerr(ip, errno, "data loss!  failed to fclose"
432                             " logfile during compact");  goto x_rc; }
433   rw->logfile= 0;
434   
435   rc= infocb(ip, rw, "compact-start", "log=%luby main=%luby",
436              logsize, (unsigned long)rw->mainsz);
437   if (rc) goto x_rc;
438
439   /* merge unsuperseded records from main into hash table */
440
441   a.mainfile= fopen(pathbuf_sfx(&rw_pbsome,".main"), "r");
442   if (!a.mainfile) PE("failed to open .main for reading during compact");
443
444   for (;;) {
445     r= readstorelogrecord(a.mainfile, &rw->logincore, ht_maybeupdate);
446     if (ferror(a.mainfile)) { rc= cht_posixerr(ip, errno, "error reading"
447                          " .main during compact"); goto x_rc;
448     }
449     if (r==-3) {
450       break;
451     } else if (r==-1 || r==-2) {
452       r= fgetpos(a.mainfile, &errpos);
453       if (r) PE("fgetpos .main during report of syntax error");
454       snprintf(buf,sizeof(buf), "CDB SYNTAX MAIN %lu", (uunsigned long)errpos);
455       Tcl_SetObjErrorCode(ip, Tcl_NewStringObj(buf,-1));
456       snprintf(buf,sizeof(buf), "%lu", (uunsigned long)errpos);
457       Tcl_ResetResult(ip);
458       Tcl_AppendResult(ip, "syntax error in .main during"
459                        " compact, at file position ", buf, (char*)0);
460       rc= TCL_ERROR;
461       goto x_rc;
462     } else {
463       assert(!rc);
464     }
465   }
466   fclose(a.mainfile);
467   a.mainfile= 0;
468
469   /* create new cdb */
470
471   cdbfd= open(pathbuf_sfx(&rw->pbsome,".tmp"), O_WRONLY|O_CREAT|O_TRUNC, 0666);
472   if (cdbfd<0) PE("create .tmp for new cdb during compact");
473
474   r= cdb_make_start(&a.cdbm, cdbfd);
475   if (r) PE("cdb_make_start during compact");
476   cdbmaking= 1;
477
478   r= ht_forall(&rw->logincore, addto_cdb, &addctx);
479   if (r) PE("cdb_make_add during compact");
480
481   r= cdb_make_finish(&a.cdbm, cdbfd);
482   if(r) PE("cdb_make_finish during compact");
483   cdbmaking= 0;
484
485   r= fdatasync(cdbfd);  if (r) PE("fdatasync new cdb during compact");
486   r= close(cdbfd);  if (r) PE("close new cdb during compact");
487   cdbfd= -1;
488
489   r= rename(rw->pbsome.buf, pathbuf_sfx(&rw->pbother,".cdb"));
490   if (r) PE("install new .cdb during compact");
491
492   /* create new main */
493
494   a.mainfile= fopen(pathbuf_sfx(&rw->pbsome,".tmp"), "w");
495   if (!a.mainfile) PE("create .tmp for new main during compact");
496
497   a.count= 0;
498   r= ht_forall(&rw->logincore, addto_main, a.mainfile);
499   if (r) { rc= cht_posixerr(ip, r, "error writing to new .main"
500                             " during compact");  goto x_rc; }
501   
502   r= fflush(a.mainfile);  if (r) PE("fflush new main during compact");
503   r= fdatasync(fileno(a.mainfile));
504   if (r) PE("fdatasync new main during compact");
505   
506   r= fclose(a.mainfile);  if (r) PE("fclose new main during compact");
507   a.mainfile= 0;
508
509   r= rename(rw->pbsome.buf, pathbuf_sfx(&rw->pbother,".main"));
510   if (r) PE("install new .main during compact");
511
512   /* done! */
513   
514   rc= infocb(ip, rw, "compact-end", "log=%luby main=%luby",
515              logsize, (unsigned long)rw->mainsz);
516   if (rc) goto x_rc;
517
518   rc= TCL_OK;
519 x_rc:
520   if (mainfile) fclose(mainfile);
521   if (cdbmaking) cdb_make_finish(&a.cdbm, cdbfd);
522   maybe_close(cdbfd);
523   remove(pathbuf_sfx(&rw->pbsome,".tmp")); /* for tidyness */
524 }
525   
526 static void compact_forclose(Tcl_Interp *ip, Rw *rw) {
527   long logsz;
528   int rc;
529
530   logsz= ftell(rw->logfile);
531   if (logsz < 0) PE("ftell logfile (during tidy close)");
532
533   rc= compact_core(ip, rw, logsz);  if (rc) goto x_rc;
534
535   r= remove(pathbuf_sfx(&rw->pbsome,".log"));
536   if (r) PE("remove .log (during tidy close)");
537 }
538   
539 int cht_do_cdbwr_close(ClientData cd, Tcl_Interp *ip, void *rw_v) {
540   Rw *rw= rw_v;
541   int rc, compact_rc, infocb_rc;
542
543   if (rw->autocompact) compact_rc= compact_forclose(ip, rw);
544   else compact_rc= TCL_OK;
545
546   rc= rw_close(ip,rw);
547   infocb_rc= infocb_close(rw);
548   
549   cht_tabledataid_disposing(ip, rw_v, &cdbtcl_rwdatabases);
550   if (!rc) rc= compact_rc;
551   if (!rc) rc= infocb_rc;
552   return rc;
553 }
554
555   
556 int cht_do_cdbwr_lookup(ClientData cd, Tcl_Interp *ip, void *db, Tcl_Obj *key, Tcl_Obj **result);
557 int cht_do_cdbwr_lookup_hb(ClientData cd, Tcl_Interp *ip, void *db, HBytes_Value key, HBytes_Value *result);
558 int cht_do_cdbwr_update(ClientData cd, Tcl_Interp *ip, void *db, Tcl_Obj *key, Tcl_Obj *value);
559 int cht_do_cdbwr_update_hb(ClientData cd, Tcl_Interp *ip, void *db, HBytes_Value key, HBytes_Value value);
560 int cht_do_cdbwr_update_quick(ClientData cd, Tcl_Interp *ip, void *db, Tcl_Obj *key, Tcl_Obj *value);
561 int cht_do_cdbwr_update_quick_hb(ClientData cd, Tcl_Interp *ip, void *db, HBytes_Value key, HBytes_Value value);