#include <string.h> #include <errno.h> #include "varint.h" #include "lmdb.h" #include "libp2p/utils/logger.h" #include "libp2p/crypto/encoding/base58.h" #include "ipfs/repo/fsrepo/journalstore.h" #include "ipfs/repo/fsrepo/lmdb_datastore.h" struct JournalRecord* lmdb_journal_record_new() { struct JournalRecord* rec = (struct JournalRecord*) malloc(sizeof(struct JournalRecord)); if (rec != NULL) { rec->hash = NULL; rec->hash_size = 0; rec->pending = 0; rec->pin = 0; rec->timestamp = 0; } return rec; } int lmdb_journal_record_free(struct JournalRecord* rec) { if (rec != NULL) { if (rec->hash != NULL) free(rec->hash); rec->hash = NULL; free(rec); } return 1; } int lmdb_journalstore_generate_key(const struct JournalRecord* journal_record, struct MDB_val *db_key) { // build the key uint8_t time_varint[8]; size_t time_varint_size = 0; varint_encode(journal_record->timestamp, &time_varint[0], 8, &time_varint_size); db_key->mv_size = time_varint_size; db_key->mv_data = time_varint; return 1; } /*** * Convert the JournalRec struct into a lmdb key and lmdb value * @param journal_record the record to convert * @param db_key where to store the key information * @param db_value where to store the value information */ int lmdb_journalstore_build_key_value_pair(const struct JournalRecord* journal_record, struct MDB_val* db_key, struct MDB_val *db_value) { // build the record, which is a timestamp as a key // build the key lmdb_journalstore_generate_key(journal_record, db_key); // build the value size_t record_size = journal_record->hash_size + 2; uint8_t record[record_size]; // Field 1: pin flag record[0] = journal_record->pin; // Field 2: pending flag record[1] = journal_record->pending; // field 3: hash memcpy(&record[2], journal_record->hash, journal_record->hash_size); db_value->mv_size = record_size; db_value->mv_data = record; return 1; } /*** * Build a JournalRecord from a key/value pair from the db * @param db_key the key * @param db_value the value * @param journal_record where to store the results * @reutrns true(1) on success, false(0) on error */ int lmdb_journalstore_build_record(const struct MDB_val* db_key, const struct MDB_val *db_value, struct JournalRecord **journal_record) { if (*journal_record == NULL) { *journal_record = lmdb_journal_record_new(); if (*journal_record == NULL) { libp2p_logger_error("lmdb_journalstore", "build_record: Unable to allocate memory for new journal record.\n"); return 0; } } struct JournalRecord *rec = *journal_record; // timestamp size_t varint_size = 0; rec->timestamp = varint_decode(db_key->mv_data, db_key->mv_size, &varint_size); // pin flag rec->pin = ((uint8_t*)db_value->mv_data)[0]; // pending flag rec->pending = ((uint8_t*)db_value->mv_data)[1]; // hash if (rec->hash != NULL) { free(rec->hash); rec->hash = NULL; rec->hash_size = 0; } rec->hash_size = db_value->mv_size - 2; rec->hash = malloc(rec->hash_size); uint8_t *val = (uint8_t*)db_value->mv_data; memcpy(rec->hash, &val[2], rec->hash_size); return 1; } /*** * Write a journal record * @param mbd_txn the transaction * @param timestamp the timestamp * @param hash the hash * @param hash_size the size of the hash * @returns true(1) on success, false(0) otherwise */ int lmdb_journalstore_journal_add(struct lmdb_trans_cursor *journalstore_cursor, struct JournalRecord *journalstore_record) { MDB_val journalstore_key; MDB_val journalstore_value; int createdTransaction = 0; if (!lmdb_journalstore_build_key_value_pair(journalstore_record, &journalstore_key, &journalstore_value)) { libp2p_logger_error("lmdbd_journalstore", "add: Unable to convert journalstore record to key/value.\n"); return 0; } // create transaction if necessary if (journalstore_cursor->transaction == NULL) { mdb_txn_begin(journalstore_cursor->environment, journalstore_cursor->parent_transaction, 0, &journalstore_cursor->transaction); createdTransaction = 1; } if (mdb_put(journalstore_cursor->transaction, *journalstore_cursor->database, &journalstore_key, &journalstore_value, 0) != 0) { libp2p_logger_error("lmdb_journalstore", "Unable to add to JOURNALSTORE database.\n"); return 0; } if (createdTransaction) { if (mdb_txn_commit(journalstore_cursor->transaction) != 0) { libp2p_logger_error("lmdb_journalstore", "Unable to commit JOURNALSTORE transaction.\n"); return 0; } } return 1; } /*** * Attempt to get a specific record identified by its timestamp and bytes * @param handle a handle to the database engine * @param journalstore_cursor the cursor (will be returned as a cursor that points to the record found) * @param journalstore_record where to put the results (can pass null). If data is within the struct, will use it as search criteria * @returns true(1) on success, false(0) otherwise */ int lmdb_journalstore_get_record(void* handle, struct lmdb_trans_cursor *journalstore_cursor, struct JournalRecord **journalstore_record) { if (handle == NULL) { libp2p_logger_error("lmdb_journalstore", "get_record: database environment not set up.\n"); return 0; } struct lmdb_context *db_context = (struct lmdb_context*)handle; // create a new transaction if necessary if (journalstore_cursor->transaction == NULL) { if (mdb_txn_begin(db_context->db_environment, journalstore_cursor->parent_transaction, 0, &journalstore_cursor->transaction) != 0) { libp2p_logger_error("lmdb_journanstore", "get_record: Attempt to begin transaction failed.\n"); return 0; } } if (journalstore_cursor->cursor == NULL) { if (!lmdb_journalstore_cursor_open(handle, &journalstore_cursor, NULL)) { libp2p_logger_error("lmdb_journalstore", "Unable to open cursor in get_record.\n"); return 0; } } // search for the timestamp if (!lmdb_journalstore_cursor_get(journalstore_cursor, CURSOR_FIRST, journalstore_record)) { libp2p_logger_debug("lmdb_journalstore", "Unable to find any records in table.\n"); return 0; } return 1; } /** * Open a cursor to the journalstore table * @param db_handle a handle to the database (an MDB_env pointer) * @param cursor where to place the results * @returns true(1) on success, false(0) otherwise */ int lmdb_journalstore_cursor_open(void *handle, struct lmdb_trans_cursor **crsr, struct MDB_txn* trans_to_use) { if (handle != NULL) { struct lmdb_context *db_context = (struct lmdb_context*)handle; struct lmdb_trans_cursor *cursor = *crsr; if (cursor == NULL ) { cursor = lmdb_trans_cursor_new(); if (cursor == NULL) return 0; *crsr = cursor; } cursor->database = db_context->journal_db; cursor->environment = db_context->db_environment; cursor->parent_transaction = db_context->current_transaction; if (cursor->transaction == NULL) { if (trans_to_use != NULL) cursor->transaction = trans_to_use; else { // open transaction if (mdb_txn_begin(db_context->db_environment, db_context->current_transaction, 0, &cursor->transaction) != 0) { libp2p_logger_error("lmdb_journalstore", "cursor_open: Unable to begin a transaction.\n"); return 0; } } } if (cursor->cursor == NULL) { // open cursor if (mdb_cursor_open(cursor->transaction, *cursor->database, &cursor->cursor) != 0) { libp2p_logger_error("lmdb_journalstore", "cursor_open: Unable to open cursor.\n"); mdb_txn_commit(cursor->transaction); return 0; } return 1; } } else { libp2p_logger_error("lmdb_journalstore", "Unable to open cursor on null db handle.\n"); } return 0; } int lmdb_journalstore_composite_key_compare(struct JournalRecord *a, struct JournalRecord *b) { if (a == NULL && b == NULL) return 0; if (a == NULL && b != NULL) return 1; if (a != NULL && b == NULL) return -1; if (a->timestamp != b->timestamp) { if (a->timestamp > b->timestamp) return -1; else return 1; } if (a->hash_size != b->hash_size) { if (a->hash_size > b->hash_size) return -1; else return 1; } for(int i = 0; i < a->hash_size; i++) { if (a->hash[i] != b->hash[i]) { return b->hash[i] - a->hash[i]; } } return 0; } /** * Read a record from the cursor. If (record) contains a key, it will look for the exact record. * If not, it will return the first matching record. * @param crsr the lmdb_trans_cursor * @param op the cursor operation (i.e. CURSOR_FIRST, CURSOR_NEXT, CURSOR_LAST, CURSOR_PREVIOUS) * @param record the record (will allocate a new one if *record is NULL) * @returns true(1) if something was found, false(0) otherwise) */ int lmdb_journalstore_cursor_get(struct lmdb_trans_cursor *tc, enum DatastoreCursorOp op, struct JournalRecord** record) { if (tc != NULL) { MDB_val mdb_key; MDB_val mdb_value; MDB_cursor_op co = MDB_FIRST; if (op == CURSOR_FIRST) co = MDB_FIRST; else if (op == CURSOR_NEXT) co = MDB_NEXT; else if (op == CURSOR_LAST) co = MDB_LAST; else if (op == CURSOR_PREVIOUS) co = MDB_PREV; if (*record != NULL) { lmdb_journalstore_generate_key(*record, &mdb_key); } int retVal = mdb_cursor_get(tc->cursor, &mdb_key, &mdb_value, co); if (retVal != 0) { if (retVal == MDB_NOTFOUND) { libp2p_logger_debug("lmdb_journalstore", "cursor_get: No records found in db.\n"); } else if (retVal == EINVAL) { libp2p_logger_debug("lmdb_journalstore", "cursor_get: Invalid parameter specified.\n"); } return 0; } if (*record == NULL) { // make a new record and pass it back if (!lmdb_journalstore_build_record(&mdb_key, &mdb_value, record)) return 0; return 1; } // see if the passed in record has a specific record in mind (take care of duplicate keys) if ( (*record)->hash_size > 0) { struct JournalRecord* curr_record = NULL; if (!lmdb_journalstore_build_record(&mdb_key, &mdb_value, &curr_record)) { libp2p_logger_error("lmdb_journalstore", "Unable to convert journalstore record into a JournalRecord struct.\n"); return 0; } // we are looking for a specific record. Flip through the records looking for the exact record while (lmdb_journalstore_composite_key_compare(*record, curr_record) != 0) { if ( (*record)->timestamp != curr_record->timestamp) { //we've exhausted all records for this timestamp. Exit. lmdb_journal_record_free(curr_record); curr_record = NULL; break; } // we did not find the exact record. Skip to the next one lmdb_journal_record_free(curr_record); curr_record = NULL; mdb_cursor_get(tc->cursor, &mdb_key, &mdb_value, MDB_NEXT); if (!lmdb_journalstore_build_record(&mdb_key, &mdb_value, &curr_record)) { libp2p_logger_error("lmdb_journalstore", "Unable to convert journalstore record into a JournalRecord struct.\n"); return 0; } } if (curr_record != NULL) { // we found the exact record. merge it into the *record (*record)->pending = curr_record->pending; (*record)->pin = curr_record->pin; lmdb_journal_record_free(curr_record); return 1; } } else { // we're not looking for any particular record. Return the first one found. return lmdb_journalstore_build_record(&mdb_key, &mdb_value, record); } } return 0; } /*** * Write the record at the cursor * @param crsr the cursor * @param journal_record the record to write * @returns true(1) on success, false(0) otherwise */ int lmdb_journalstore_cursor_put(struct lmdb_trans_cursor *crsr, struct JournalRecord* journal_record) { struct MDB_cursor* cursor = crsr->cursor; struct MDB_val db_key; struct MDB_val db_value; if (!lmdb_journalstore_build_key_value_pair(journal_record, &db_key, &db_value)) { libp2p_logger_error("lmdb_journalstore", "Unable to create journalstore record.\n"); return 0; } int retVal = mdb_cursor_put(cursor, &db_key, &db_value, 0); if (retVal != 0) { char* result = ""; switch (retVal) { case(MDB_MAP_FULL): result = "Database Full"; break; case (MDB_TXN_FULL): result = "Transaction has too many dirty pages"; break; case (EACCES) : result = "Attempt was made to write in a read only transaction"; break; case (EINVAL) : result = "An invalid parameter was specified"; break; } libp2p_logger_error("lmdb_journalstore", "Put failed with error message %d [%s].\n", retVal, result); return 0; } return 1; } /** * Close the cursor and commits the transaction. * @param crsr a lmdb_trans_cursor pointer * @returns true(1) */ int lmdb_journalstore_cursor_close(struct lmdb_trans_cursor *cursor, int commitTransaction) { if (cursor != NULL) { if (cursor->cursor != NULL) { //mdb_cursor_close(cursor->cursor); } if (cursor->transaction != NULL && commitTransaction) { mdb_txn_commit(cursor->transaction); } cursor->cursor = NULL; cursor->transaction = NULL; lmdb_trans_cursor_free(cursor); } return 1; }