static int set_new_value(BaseTDBResolver *self, TDB_DATA urn, TDB_DATA attribute, TDB_DATA value) { TDB_DATA key,offset; char buff[BUFF_SIZE]; char buff2[BUFF_SIZE]; uint32_t new_offset; TDB_DATA_LIST i; // Update the value in the db and replace with new value key.dptr = (unsigned char *)buff; key.dsize = calculate_key(self, urn, attribute, buff, BUFF_SIZE, 1); // Lock the database tdb_lockall(self->data_db); // Go to the end and write the new record new_offset = lseek(self->data_store_fd, 0, SEEK_END); // The offset to the next item in the list i.offset = 0; i.length = value.dsize; write(self->data_store_fd, &i, sizeof(i)); write(self->data_store_fd, value.dptr, value.dsize); offset.dptr = (unsigned char *)buff2; offset.dsize = from_int(new_offset, buff2, BUFF_SIZE); tdb_store(self->data_db, key, offset, TDB_REPLACE); //Done tdb_unlockall(self->data_db); return 1; };
static PyObject *obj_unlockall(PyTdbObject *self) { int ret; PyErr_TDB_RAISE_IF_CLOSED(self); ret = tdb_unlockall(self->ctx); PyErr_TDB_ERROR_IS_ERR_RAISE(ret, self->ctx); Py_RETURN_NONE; }
/** Fetches the id for the given key from the database tdb - if create_new is set and there is no id present, we create a new id and return id. */ static uint32_t get_id(struct tdb_context *tdb, TDB_DATA key, int create_new) { char buff[BUFF_SIZE]; TDB_DATA urn_id; uint32_t max_id=0; uint32_t result=0; /* We get given an ID and we retrieve the URN it belongs to */ tdb_lockall(tdb); urn_id = tdb_fetch(tdb, key); if(urn_id.dptr) { result = to_int(urn_id); free(urn_id.dptr); tdb_unlockall(tdb); return result; } else if(create_new) { TDB_DATA max_key; max_key.dptr = (unsigned char *)MAX_KEY; max_key.dsize = strlen(MAX_KEY); urn_id = tdb_fetch(tdb, max_key); if(urn_id.dptr) { max_id = to_int(urn_id); free(urn_id.dptr); }; max_id++; // Update the new MAX_KEY urn_id.dptr = (unsigned char *)buff; urn_id.dsize = from_int(max_id, buff, BUFF_SIZE); tdb_store(tdb, key, urn_id, TDB_REPLACE); tdb_store(tdb, max_key, urn_id, TDB_REPLACE); tdb_store(tdb, urn_id, key, TDB_REPLACE); tdb_unlockall(tdb); return max_id; }; tdb_unlockall(tdb); // This should never happen return 0; };
PyObject *py_tdb_hnd_unlock_all(PyObject *self, PyObject *args) { tdb_hnd_object *obj = (tdb_hnd_object *)self; if (!obj->tdb) { PyErr_SetString(py_tdb_error, "tdb object has been closed"); return NULL; } tdb_unlockall(obj->tdb); Py_INCREF(Py_None); return Py_None; }
/* load a msg file into the tdb */ static BOOL load_msg(const char *msg_file) { char **lines; int num_lines, i; char *msgid, *msgstr; TDB_DATA key, data; lines = file_lines_load(msg_file, &num_lines); if (!lines) { return False; } if (tdb_lockall(tdb) != 0) return False; /* wipe the db */ tdb_traverse(tdb, tdb_traverse_delete_fn, NULL); msgid = NULL; for (i=0;i<num_lines;i++) { if (strncmp(lines[i], "msgid \"", 7) == 0) { msgid = lines[i] + 7; } if (msgid && strncmp(lines[i], "msgstr \"", 8) == 0) { msgstr = lines[i] + 8; trim_char(msgid, '\0', '\"'); trim_char(msgstr, '\0', '\"'); if (*msgstr == 0) { msgstr = msgid; } all_string_sub(msgid, "\\n", "\n", 0); all_string_sub(msgstr, "\\n", "\n", 0); key.dptr = msgid; key.dsize = strlen(msgid)+1; data.dptr = msgstr; data.dsize = strlen(msgstr)+1; tdb_store(tdb, key, data, 0); msgid = NULL; } } file_lines_free(lines); tdb_unlockall(tdb); return True; }
/* load a msg file into the tdb */ static bool load_msg(const char *msg_file) { char **lines; int num_lines, i; char *msgid, *msgstr; TDB_DATA data; lines = file_lines_load(msg_file, &num_lines, 0, NULL); if (!lines) { return False; } if (tdb_lockall(tdb) != 0) { TALLOC_FREE(lines); return False; } /* wipe the db */ tdb_wipe_all(tdb); msgid = NULL; for (i=0;i<num_lines;i++) { if (strncmp(lines[i], "msgid \"", 7) == 0) { msgid = lines[i] + 7; } if (msgid && strncmp(lines[i], "msgstr \"", 8) == 0) { msgstr = lines[i] + 8; trim_char(msgid, '\0', '\"'); trim_char(msgstr, '\0', '\"'); if (*msgstr == 0) { msgstr = msgid; } all_string_sub(msgid, "\\n", "\n", 0); all_string_sub(msgstr, "\\n", "\n", 0); data = string_term_tdb_data(msgstr); tdb_store_bystring(tdb, msgid, data, 0); msgid = NULL; } } TALLOC_FREE(lines); tdb_unlockall(tdb); return True; }
static PyObject *add(BaseTDBResolver *self, PyObject *args, PyObject *kwds) { char buff[BUFF_SIZE]; char buff2[BUFF_SIZE]; static char *kwlist[] = {"urn", "attribute", "value", "unique", NULL}; TDB_DATA urn; TDB_DATA attribute; TDB_DATA key; PyObject *value_obj, *value_str; TDB_DATA value; TDB_DATA offset; TDB_DATA_LIST i; uint32_t previous_offset=0; uint32_t new_offset; int unique = 0; attribute.dsize = 0; urn.dsize = 0; if(!PyArg_ParseTupleAndKeywords(args, kwds, "s#s#O|L", kwlist, &urn.dptr, &urn.dsize, &attribute.dptr, &attribute.dsize, &value_obj, &unique)) return NULL; // Convert the object to a string value_str = PyObject_Str(value_obj); if(!value_str) return NULL; PyString_AsStringAndSize(value_str, (char **)&value.dptr, &value.dsize); /** If the value is already in the list, we just ignore this request. */ if(unique && is_value_present(self, urn, attribute, value, 1)) { goto exit; }; // Ok if we get here, the value is not already stored there. key.dptr = (unsigned char *)buff; key.dsize = calculate_key(self, urn, attribute, buff, BUFF_SIZE, 1); // Lock the data_db to synchronise access to the store: tdb_lockall(self->data_db); offset = tdb_fetch(self->data_db, key); if(offset.dptr) { previous_offset = to_int(offset); free(offset.dptr); }; // Go to the end and write the new record new_offset = lseek(self->data_store_fd, 0, SEEK_END); i.offset = previous_offset; i.length = value.dsize; write(self->data_store_fd, &i, sizeof(i)); write(self->data_store_fd, value.dptr, value.dsize); // Now store the offset to this in the tdb database value.dptr = (unsigned char *)buff2; value.dsize = from_int(new_offset, buff2, BUFF_SIZE); tdb_store(self->data_db, key, value, TDB_REPLACE); // Done tdb_unlockall(self->data_db); exit: Py_DECREF(value_str); Py_RETURN_NONE; };
static int tdbresolver_init(BaseTDBResolver *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"path", "hashsize", NULL}; char buff[BUFF_SIZE]; char *path = "."; int flags = O_RDWR | O_CREAT; if(!PyArg_ParseTupleAndKeywords(args, kwds, "|sL", kwlist, &path, &self->hashsize)) return -1; if(snprintf(buff, BUFF_SIZE, "%s/urn.tdb", path) >= BUFF_SIZE) goto error; self->urn_db = tdb_open(buff, self->hashsize, 0, O_RDWR | O_CREAT, 0644); if(!self->urn_db) goto error; if(snprintf(buff, BUFF_SIZE, "%s/attribute.tdb", path) >= BUFF_SIZE) goto error1; self->attribute_db = tdb_open(buff, self->hashsize, 0, O_RDWR | O_CREAT, 0644); if(!self->attribute_db) goto error1; if(snprintf(buff, BUFF_SIZE, "%s/data.tdb", path) >= BUFF_SIZE) goto error2; self->data_db = tdb_open(buff, self->hashsize, 0, O_RDWR | O_CREAT, 0644); if(!self->data_db) goto error2; if(snprintf(buff, BUFF_SIZE, "%s/data_store.tdb", path) >= BUFF_SIZE) goto error3; self->data_store_fd = open(buff, O_RDWR | O_CREAT, 0644); if(self->data_store_fd < 0) goto error3; // This ensures that the data store never has an offset of 0 (This // indicates an error) // Access to the tdb_store is managed via locks on the data.tdb tdb_lockall(self->data_db); if(lseek(self->data_store_fd, 0, SEEK_END)==0) { (void)write(self->data_store_fd, "data",4); }; tdb_unlockall(self->data_db); return 0; error3: tdb_close(self->data_db); self->data_db = NULL; error2: tdb_close(self->attribute_db); self->attribute_db = NULL; error1: tdb_close(self->urn_db); self->urn_db = NULL; error: PyErr_Format(PyExc_IOError, "Unable to open tdb files"); return -1; };
static PyObject *pytdb_unlock(PyTDB *self, PyObject *args, PyObject *kwds) { tdb_unlockall(self->context); Py_RETURN_NONE; }
/* carefully backup a tdb, validating the contents and only doing the backup if its OK this function is also used for restore */ static int backup_tdb(const char *old_name, const char *new_name, int hash_size, int nolock) { TDB_CONTEXT *tdb; TDB_CONTEXT *tdb_new; char *tmp_name; struct stat st; int count1, count2; tmp_name = add_suffix(new_name, ".tmp"); /* stat the old tdb to find its permissions */ if (stat(old_name, &st) != 0) { perror(old_name); free(tmp_name); return 1; } /* open the old tdb */ tdb = tdb_open_ex(old_name, 0, TDB_DEFAULT | (nolock ? TDB_NOLOCK : 0), O_RDWR, 0, &log_ctx, NULL); if (!tdb) { printf("Failed to open %s\n", old_name); free(tmp_name); return 1; } /* create the new tdb */ unlink(tmp_name); tdb_new = tdb_open_ex(tmp_name, hash_size ? hash_size : tdb_hash_size(tdb), TDB_DEFAULT, O_RDWR|O_CREAT|O_EXCL, st.st_mode & 0777, &log_ctx, NULL); if (!tdb_new) { perror(tmp_name); free(tmp_name); return 1; } if (tdb_transaction_start(tdb) != 0) { printf("Failed to start transaction on old tdb\n"); tdb_close(tdb); tdb_close(tdb_new); unlink(tmp_name); free(tmp_name); return 1; } /* lock the backup tdb so that nobody else can change it */ if (tdb_lockall(tdb_new) != 0) { printf("Failed to lock backup tdb\n"); tdb_close(tdb); tdb_close(tdb_new); unlink(tmp_name); free(tmp_name); return 1; } failed = 0; /* traverse and copy */ count1 = tdb_traverse(tdb, copy_fn, (void *)tdb_new); if (count1 < 0 || failed) { fprintf(stderr,"failed to copy %s\n", old_name); tdb_close(tdb); tdb_close(tdb_new); unlink(tmp_name); free(tmp_name); return 1; } /* close the old tdb */ tdb_close(tdb); /* copy done, unlock the backup tdb */ tdb_unlockall(tdb_new); #ifdef HAVE_FDATASYNC if (fdatasync(tdb_fd(tdb_new)) != 0) { #else if (fsync(tdb_fd(tdb_new)) != 0) { #endif /* not fatal */ fprintf(stderr, "failed to fsync backup file\n"); } /* close the new tdb and re-open read-only */ tdb_close(tdb_new); tdb_new = tdb_open_ex(tmp_name, 0, TDB_DEFAULT, O_RDONLY, 0, &log_ctx, NULL); if (!tdb_new) { fprintf(stderr,"failed to reopen %s\n", tmp_name); unlink(tmp_name); perror(tmp_name); free(tmp_name); return 1; } /* traverse the new tdb to confirm */ count2 = tdb_traverse(tdb_new, test_fn, NULL); if (count2 != count1) { fprintf(stderr,"failed to copy %s\n", old_name); tdb_close(tdb_new); unlink(tmp_name); free(tmp_name); return 1; } /* close the new tdb and rename it to .bak */ tdb_close(tdb_new); if (rename(tmp_name, new_name) != 0) { perror(new_name); free(tmp_name); return 1; } free(tmp_name); return 0; } /* verify a tdb and if it is corrupt then restore from *.bak */ static int verify_tdb(const char *fname, const char *bak_name) { TDB_CONTEXT *tdb; int count = -1; /* open the tdb */ tdb = tdb_open_ex(fname, 0, 0, O_RDONLY, 0, &log_ctx, NULL); /* traverse the tdb, then close it */ if (tdb) { count = tdb_traverse(tdb, test_fn, NULL); tdb_close(tdb); } /* count is < 0 means an error */ if (count < 0) { printf("restoring %s\n", fname); return backup_tdb(bak_name, fname, 0, 0); } printf("%s : %d records\n", fname, count); return 0; }
/* wipe the entire database, deleting all records. This can be done very fast by using a allrecord lock. The entire data portion of the file becomes a single entry in the freelist. This code carefully steps around the recovery area, leaving it alone */ int tdb1_wipe_all(struct tdb_context *tdb) { int i; tdb1_off_t offset = 0; ssize_t data_len; tdb1_off_t recovery_head; tdb1_len_t recovery_size = 0; if (tdb_lockall(tdb) != TDB_SUCCESS) { return -1; } /* see if the tdb has a recovery area, and remember its size if so. We don't want to lose this as otherwise each tdb1_wipe_all() in a transaction will increase the size of the tdb by the size of the recovery area */ if (tdb1_ofs_read(tdb, TDB1_RECOVERY_HEAD, &recovery_head) == -1) { tdb_logerr(tdb, tdb->last_error, TDB_LOG_ERROR, "tdb1_wipe_all: failed to read recovery head"); goto failed; } if (recovery_head != 0) { struct tdb1_record rec; if (tdb->tdb1.io->tdb1_read(tdb, recovery_head, &rec, sizeof(rec), TDB1_DOCONV()) == -1) { tdb_logerr(tdb, tdb->last_error, TDB_LOG_ERROR, "tdb1_wipe_all: failed to read recovery record"); return -1; } recovery_size = rec.rec_len + sizeof(rec); } /* wipe the hashes */ for (i=0;i<tdb->tdb1.header.hash_size;i++) { if (tdb1_ofs_write(tdb, TDB1_HASH_TOP(i), &offset) == -1) { tdb_logerr(tdb, tdb->last_error, TDB_LOG_ERROR, "tdb1_wipe_all: failed to write hash %d", i); goto failed; } } /* wipe the freelist */ if (tdb1_ofs_write(tdb, TDB1_FREELIST_TOP, &offset) == -1) { tdb_logerr(tdb, tdb->last_error, TDB_LOG_ERROR, "tdb1_wipe_all: failed to write freelist"); goto failed; } /* add all the rest of the file to the freelist, possibly leaving a gap for the recovery area */ if (recovery_size == 0) { /* the simple case - the whole file can be used as a freelist */ data_len = (tdb->file->map_size - TDB1_DATA_START(tdb->tdb1.header.hash_size)); if (tdb1_free_region(tdb, TDB1_DATA_START(tdb->tdb1.header.hash_size), data_len) != 0) { goto failed; } } else { /* we need to add two freelist entries - one on either side of the recovery area Note that we cannot shift the recovery area during this operation. Only the transaction.c code may move the recovery area or we risk subtle data corruption */ data_len = (recovery_head - TDB1_DATA_START(tdb->tdb1.header.hash_size)); if (tdb1_free_region(tdb, TDB1_DATA_START(tdb->tdb1.header.hash_size), data_len) != 0) { goto failed; } /* and the 2nd free list entry after the recovery area - if any */ data_len = tdb->file->map_size - (recovery_head+recovery_size); if (tdb1_free_region(tdb, recovery_head+recovery_size, data_len) != 0) { goto failed; } } tdb1_increment_seqnum_nonblock(tdb); tdb_unlockall(tdb); return 0; failed: tdb_unlockall(tdb); return -1; }
int tdb_check(struct tdb_context *tdb, int (*check)(TDB_DATA key, TDB_DATA data, void *private_data), void *private_data) { unsigned int h; unsigned char **hashes; tdb_off_t off, recovery_start; struct tdb_record rec; bool found_recovery = false; if (tdb_lockall(tdb) == -1) return -1; /* Make sure we know true size of the underlying file. */ tdb->methods->tdb_oob(tdb, tdb->map_size + 1, 1); /* Header must be OK: also gets us the recovery ptr, if any. */ if (!tdb_check_header(tdb, &recovery_start)) goto unlock; /* We should have the whole header, too. */ if (tdb->map_size < TDB_DATA_START(tdb->header.hash_size)) { tdb->ecode = TDB_ERR_CORRUPT; TDB_LOG((tdb, TDB_DEBUG_ERROR, "File too short for hashes\n")); goto unlock; } /* One big malloc: pointers then bit arrays. */ hashes = (unsigned char **)calloc( 1, sizeof(hashes[0]) * (1+tdb->header.hash_size) + BITMAP_BITS / CHAR_BIT * (1+tdb->header.hash_size)); if (!hashes) { tdb->ecode = TDB_ERR_OOM; goto unlock; } /* Initialize pointers */ hashes[0] = (unsigned char *)(&hashes[1+tdb->header.hash_size]); for (h = 1; h < 1+tdb->header.hash_size; h++) hashes[h] = hashes[h-1] + BITMAP_BITS / CHAR_BIT; /* Freelist and hash headers are all in a row: read them. */ for (h = 0; h < 1+tdb->header.hash_size; h++) { if (tdb_ofs_read(tdb, FREELIST_TOP + h*sizeof(tdb_off_t), &off) == -1) goto free; if (off) record_offset(hashes[h], off); } /* For each record, read it in and check it's ok. */ for (off = TDB_DATA_START(tdb->header.hash_size); off < tdb->map_size; off += sizeof(rec) + rec.rec_len) { if (tdb->methods->tdb_read(tdb, off, &rec, sizeof(rec), DOCONV()) == -1) goto free; switch (rec.magic) { case TDB_MAGIC: case TDB_DEAD_MAGIC: if (!tdb_check_used_record(tdb, off, &rec, hashes, check, private_data)) goto free; break; case TDB_FREE_MAGIC: if (!tdb_check_free_record(tdb, off, &rec, hashes)) goto free; break; case TDB_RECOVERY_MAGIC: case 0: /* Used for invalid (or in-progress) recovery area. */ if (recovery_start != off) { TDB_LOG((tdb, TDB_DEBUG_ERROR, "Unexpected recovery record at offset %d\n", off)); goto free; } found_recovery = true; break; default: tdb->ecode = TDB_ERR_CORRUPT; TDB_LOG((tdb, TDB_DEBUG_ERROR, "Bad magic 0x%x at offset %d\n", rec.magic, off)); goto free; } } /* Now, hashes should all be empty: each record exists and is referred * to by one other. */ for (h = 0; h < 1+tdb->header.hash_size; h++) { unsigned int i; for (i = 0; i < BITMAP_BITS / CHAR_BIT; i++) { if (hashes[h][i] != 0) { tdb->ecode = TDB_ERR_CORRUPT; TDB_LOG((tdb, TDB_DEBUG_ERROR, "Hashes do not match records\n")); goto free; } } } /* We must have found recovery area if there was one. */ if (recovery_start != 0 && !found_recovery) { TDB_LOG((tdb, TDB_DEBUG_ERROR, "Expected %s recovery area, got %s\n", recovery_start ? "a" : "no", found_recovery ? "one" : "none")); goto free; } free(hashes); tdb_unlockall(tdb); return 0; free: free(hashes); unlock: tdb_unlockall(tdb); return -1; }
int main(int argc, char *argv[]) { unsigned int i, extra_msgs; struct tdb_context *tdb; struct tdb_data key = tdb_mkdata("key", 3); struct tdb_data data = tdb_mkdata("data", 4); int flags[] = { TDB_DEFAULT, TDB_NOMMAP, TDB_CONVERT, TDB_NOMMAP|TDB_CONVERT, TDB_VERSION1, TDB_NOMMAP|TDB_VERSION1, TDB_CONVERT|TDB_VERSION1, TDB_NOMMAP|TDB_CONVERT|TDB_VERSION1 }; plan_tests(sizeof(flags) / sizeof(flags[0]) * 48); for (i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { /* RW -> R0 */ tdb = tdb_open("run-92-get-set-readonly.tdb", flags[i], O_RDWR|O_CREAT|O_TRUNC, 0600, &tap_log_attr); ok1(tdb); ok1(!(tdb_get_flags(tdb) & TDB_RDONLY)); /* TDB1 complains multiple times. */ if (flags[i] & TDB_VERSION1) { extra_msgs = 1; } else { extra_msgs = 0; } ok1(tdb_store(tdb, key, data, TDB_INSERT) == TDB_SUCCESS); tdb_add_flag(tdb, TDB_RDONLY); ok1(tdb_get_flags(tdb) & TDB_RDONLY); /* Can't store, append, delete. */ ok1(tdb_store(tdb, key, data, TDB_MODIFY) == TDB_ERR_RDONLY); ok1(tap_log_messages == 1); ok1(tdb_append(tdb, key, data) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 2); ok1(tdb_delete(tdb, key) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 3); /* Can't start a transaction, or any write lock. */ ok1(tdb_transaction_start(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 4); ok1(tdb_chainlock(tdb, key) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 5); ok1(tdb_lockall(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 6); ok1(tdb_wipe_all(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 7); /* Back to RW. */ tdb_remove_flag(tdb, TDB_RDONLY); ok1(!(tdb_get_flags(tdb) & TDB_RDONLY)); ok1(tdb_store(tdb, key, data, TDB_MODIFY) == TDB_SUCCESS); ok1(tdb_append(tdb, key, data) == TDB_SUCCESS); ok1(tdb_delete(tdb, key) == TDB_SUCCESS); ok1(tdb_transaction_start(tdb) == TDB_SUCCESS); ok1(tdb_store(tdb, key, data, TDB_INSERT) == TDB_SUCCESS); ok1(tdb_transaction_commit(tdb) == TDB_SUCCESS); ok1(tdb_chainlock(tdb, key) == TDB_SUCCESS); tdb_chainunlock(tdb, key); ok1(tdb_lockall(tdb) == TDB_SUCCESS); tdb_unlockall(tdb); ok1(tdb_wipe_all(tdb) == TDB_SUCCESS); ok1(tap_log_messages == 7); tdb_close(tdb); /* R0 -> RW */ tdb = tdb_open("run-92-get-set-readonly.tdb", flags[i], O_RDONLY, 0600, &tap_log_attr); ok1(tdb); ok1(tdb_get_flags(tdb) & TDB_RDONLY); /* Can't store, append, delete. */ ok1(tdb_store(tdb, key, data, TDB_INSERT) == TDB_ERR_RDONLY); ok1(tap_log_messages == 8); ok1(tdb_append(tdb, key, data) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 9); ok1(tdb_delete(tdb, key) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 10); /* Can't start a transaction, or any write lock. */ ok1(tdb_transaction_start(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 11); ok1(tdb_chainlock(tdb, key) == TDB_ERR_RDONLY); tap_log_messages -= extra_msgs; ok1(tap_log_messages == 12); ok1(tdb_lockall(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 13); ok1(tdb_wipe_all(tdb) == TDB_ERR_RDONLY); ok1(tap_log_messages == 14); /* Can't remove TDB_RDONLY since we opened with O_RDONLY */ tdb_remove_flag(tdb, TDB_RDONLY); ok1(tap_log_messages == 15); ok1(tdb_get_flags(tdb) & TDB_RDONLY); tdb_close(tdb); ok1(tap_log_messages == 15); tap_log_messages = 0; } return exit_status(); }