+
+ r= lstat(pathbuf_sfx(&pbmain, ".main"), &stab);
+ if (!r) { rc= cht_staticerr(ip, "database already exists during creation",
+ "CDB ALREADY-EXISTS"); goto x_rc; }
+ if (errno != ENOENT) PE("check for existing database .main during creation");
+
+ for (toremove=toremoves; *toremove; toremove++) {
+ r= remove(pathbuf_sfx(&pb, *toremove));
+ if (r && errno != ENOENT)
+ PE("delete possible spurious file during creation");
+ }
+
+ f= fopen(pathbuf_sfx(&pb, ".tmp"), "w");
+ if (!f) PE("create new database .tmp");
+ r= putc('\n', f); if (r==EOF) PE("write sentinel to new database .tmp");
+ r= fclose(f); f=0; if (r) PE("close new database .tmp during creation");
+
+ r= rename(pb.buf, pbmain.buf);
+ if (r) PE("install new database .tmp as .main (finalising creation)");
+
+ rc= TCL_OK;
+
+ x_rc:
+ if (f) fclose(f);
+ maybe_close(lock_fd);
+ pathbuf_free(&pb);
+ pathbuf_free(&pbmain);
+ return rc;
+}
+
+/*---------- Info callbacks ----------*/
+
+static int infocbv3(Tcl_Interp *ip, Rw *rw, const char *arg1,
+ const char *arg2fmt, const char *arg3, va_list al) {
+ Tcl_Obj *aa[3];
+ int na;
+ char buf[200];
+ vsnprintf(buf, sizeof(buf), arg2fmt, al);
+
+ na= 0;
+ aa[na++]= cht_ret_string(ip, arg1);
+ aa[na++]= cht_ret_string(ip, buf);
+ if (arg3) aa[na++]= cht_ret_string(ip, arg3);
+
+ return cht_scriptinv_invoke_fg(&rw->on_info, na, aa);
+}
+
+static int infocb3(Tcl_Interp *ip, Rw *rw, const char *arg1,
+ const char *arg2fmt, const char *arg3, ...) {
+ int rc;
+ va_list al;
+ va_start(al, arg3);
+ rc= infocbv3(ip,rw,arg1,arg2fmt,arg3,al);
+ va_end(al);
+ return rc;
+}
+
+static int infocb(Tcl_Interp *ip, Rw *rw, const char *arg1,
+ const char *arg2fmt, ...) {
+ int rc;
+ va_list al;
+ va_start(al, arg2fmt);
+ rc= infocbv3(ip,rw,arg1,arg2fmt,0,al);
+ va_end(al);
+ return rc;
+}
+
+/*---------- Opening ----------*/
+
+static int cdbinit(Tcl_Interp *ip, Rw *rw) {
+ /* On entry, cdb_fd >=0 but cdb is _undefined_/
+ * On exit, either cdb_fd<0 or cdb is initialised */
+ int r, rc;
+
+ r= cdb_init(&rw->cdb, rw->cdb_fd);
+ if (r) {
+ rc= cht_posixerr(ip, errno, "failed to initialise cdb reader");
+ close(rw->cdb_fd); rw->cdb_fd= -1; return rc;
+ }
+ return TCL_OK;
+}
+
+int cht_do_cdbwr_open(ClientData cd, Tcl_Interp *ip, const char *pathb,
+ Tcl_Obj *on_info, Tcl_Obj *on_lexminval,
+ void **result) {
+ const Cdbwr_SubCommand *subcmd= cd;
+ int r, rc, mainfd=-1;
+ Rw *rw;
+ struct stat stab;
+ off_t logrecstart, logjunkpos;
+
+ rw= TALLOC(sizeof(*rw));
+ rw->ix= -1;
+ ht_setup(&rw->logincore);
+ cht_scriptinv_init(&rw->on_info);
+ cht_scriptinv_init(&rw->on_lexminval);
+ rw->cdb_fd= rw->lock_fd= -1; rw->logfile= 0;
+ pathbuf_init(&rw->pbsome, pathb);
+ pathbuf_init(&rw->pbother, pathb);
+ rw->autocompact= 1;
+
+ rc= cht_scriptinv_set(&rw->on_info, ip, on_info, 0);
+ if (rc) goto x_rc;
+
+ rc= cht_scriptinv_set(&rw->on_lexminval, ip, on_lexminval, 0);
+ if (rc) goto x_rc;
+
+ mainfd= open(pathbuf_sfx(&rw->pbsome,".main"), O_RDONLY);
+ if (mainfd<0) PE("open existing database file .main");
+ rc= acquire_lock(ip, &rw->pbsome, &rw->lock_fd); if (rc) goto x_rc;
+
+ r= fstat(mainfd, &stab); if (r) PE("fstat .main");
+ rw->mainsz= stab.st_size;
+
+ rw->cdb_fd= open(pathbuf_sfx(&rw->pbsome,".cdb"), O_RDONLY);
+ if (rw->cdb_fd >=0) {
+ rc= cdbinit(ip, rw); if (rc) goto x_rc;
+ } else if (errno == ENOENT) {
+ if (rw->mainsz > 1) {
+ rc= cht_staticerr(ip, ".cdb does not exist but .main is >1byte -"
+ " .cdb must have been accidentally deleted!",
+ "CDB CDBMISSING");
+ goto x_rc;
+ }
+ /* fine */
+ } else {
+ PE("open .cdb");
+ }
+
+ rw->logfile= fopen(pathbuf_sfx(&rw->pbsome,".jrn"), "r+");
+ if (!rw->logfile) {
+ if (errno != ENOENT) PE("failed to open .jrn during open");
+ rw->logfile= fopen(rw->pbsome.buf, "w");
+ if (!rw->logfile) PE("create .jrn during (clean) open");
+ } else { /* rw->logfile */
+ r= fstat(fileno(rw->logfile), &stab);
+ if (r==-1) PE("fstat .jrn during open");
+ rc= infocb(ip, rw, "open-dirty-start", "log=%luby",
+ (unsigned long)stab.st_size);
+ if (rc) goto x_rc;
+
+ for (;;) {
+ logrecstart= ftello(rw->logfile);
+ if (logrecstart < 0) PE("ftello .jrn during (dirty) open");
+ r= readstorelogrecord(rw->logfile, &rw->logincore, 0,0, ht_update);
+ if (ferror(rw->logfile)) {
+ rc= cht_posixerr(ip, errno, "error reading .jrn during (dirty) open");
+ goto x_rc;
+ }
+ if (r==-1) {
+ break;
+ } else if (r==-2 || r==-3) {
+ char buf[100];
+ logjunkpos= ftello(rw->logfile);
+ if(logjunkpos<0) PE("ftello .jrn during report of junk in dirty open");
+
+ snprintf(buf,sizeof(buf), "CDB SYNTAX LOG %lu %lu",
+ (unsigned long)logjunkpos, (unsigned long)logrecstart);
+
+ if (!(subcmd->flags & RWSCF_OKJUNK)) {
+ Tcl_SetObjErrorCode(ip, Tcl_NewStringObj(buf,-1));
+ snprintf(buf,sizeof(buf),"%lu",(unsigned long)logjunkpos);
+ Tcl_ResetResult(ip);
+ Tcl_AppendResult(ip, "syntax error (junk) in .jrn during"
+ " (dirty) open, at file position ", buf, (char*)0);
+ rc= TCL_ERROR;
+ goto x_rc;
+ }
+ rc= infocb3(ip, rw, "open-dirty-junk", "errorfpos=%luby", buf,
+ (unsigned long)logjunkpos);
+ if (rc) goto x_rc;
+
+ r= fseeko(rw->logfile, logrecstart, SEEK_SET);
+ if (r) PE("failed to fseeko .jrn before junk during dirty open");
+
+ r= ftruncate(fileno(rw->logfile), logrecstart);
+ if (r) PE("ftruncate .jrn to chop junk during dirty open");
+ } else {
+ assert(!r);
+ }
+ }
+ }
+ /* now log is positioned for appending and everything is read */
+
+ *result= rw;
+ maybe_close(mainfd);
+ return TCL_OK;
+
+ x_rc:
+ rw_close(0,rw);
+ TFREE(rw);
+ maybe_close(mainfd);
+ return rc;
+}
+
+int cht_do_cdbwr_open_okjunk(ClientData cd, Tcl_Interp *ip, const char *pathb,
+ Tcl_Obj *on_info, Tcl_Obj *on_lexminval,
+ void **result) {
+ return cht_do_cdbwr_open(cd,ip,pathb,on_info,on_lexminval,result);
+}
+
+/*==================== COMPACTION ====================*/
+
+struct ht_forall_ctx {
+ struct cdb_make cdbm;
+ FILE *mainfile;
+ long *reccount;
+ int lexminvall;
+ const char *lexminval; /* may be invalid if lexminvall <= 0 */
+};
+
+/*---------- helper functions ----------*/
+
+static int expiredp(const HashValue *val, struct ht_forall_ctx *a) {
+ int r, l;
+ if (!val->len || a->lexminvall<=0) return 0;
+ l= val->len < a->lexminvall ? val->len : a->lexminvall;
+ r= memcmp(val->data, a->lexminval, l);
+ if (r>0) return 0;
+ if (r<0) return 1;
+ return val->len < a->lexminvall;
+}
+
+static int delete_ifexpired(const char *key, HashValue *val,
+ struct ht_forall_ctx *a) {
+ if (!expiredp(val, a)) return 0;
+ val->len= 0;
+ /* we don't actually need to realloc it to free the memory because
+ * this will shortly all be deleted as part of the compaction */
+ return 0;
+}
+
+static int addto_cdb(const char *key, HashValue *val,
+ struct ht_forall_ctx *a) {
+ return cdb_make_add(&a->cdbm, key, strlen(key), val->data, val->len);
+}
+
+static int addto_main(const char *key, HashValue *val,
+ struct ht_forall_ctx *a) {
+ (*a->reccount)++;
+ return writerecord(a->mainfile, key, val);
+}
+
+/*---------- compact main entrypoint ----------*/
+
+static int compact_core(Tcl_Interp *ip, Rw *rw, unsigned long logsz,
+ long *reccount_r) {
+ /* creates new .cdb and .main
+ * closes logfile
+ * leaves .jrn with old data
+ * leaves cdb fd open onto old db
+ * leaves logincore full of crap
+ */
+ int r, rc;
+ int cdbfd, cdbmaking;
+ off_t errpos, newmainsz;
+ char buf[100];
+ Tcl_Obj *res;
+ struct ht_forall_ctx a;
+
+ a.mainfile= 0;
+ cdbfd= -1;
+ cdbmaking= 0;
+ *reccount_r= 0;
+ a.reccount= reccount_r;
+
+ r= fclose(rw->logfile);
+ if (r) { rc= cht_posixerr(ip, errno, "probable data loss! failed to fclose"
+ " logfile during compact"); goto x_rc; }
+ rw->logfile= 0;