michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * Berkeley DB 1.85 Shim code to handle blobs. michael@0: */ michael@0: #include "mcom_db.h" michael@0: #include "secitem.h" michael@0: #include "nssb64.h" michael@0: #include "blapi.h" michael@0: #include "secerr.h" michael@0: michael@0: #include "lgdb.h" michael@0: michael@0: /* michael@0: * Blob block: michael@0: * Byte 0 CERTDB Version -+ -+ michael@0: * Byte 1 certDBEntryTypeBlob | BLOB_HEAD_LEN | michael@0: * Byte 2 flags (always '0'); | | michael@0: * Byte 3 reserved (always '0'); -+ | michael@0: * Byte 4 LSB length | <--BLOB_LENGTH_START | BLOB_BUF_LEN michael@0: * Byte 5 . | | michael@0: * Byte 6 . | BLOB_LENGTH_LEN | michael@0: * Byte 7 MSB length | | michael@0: * Byte 8 blob_filename -+ -+ <-- BLOB_NAME_START | michael@0: * Byte 9 . | BLOB_NAME_LEN | michael@0: * . . | | michael@0: * Byte 37 . -+ -+ michael@0: */ michael@0: #define DBS_BLOCK_SIZE (16*1024) /* 16 k */ michael@0: #define DBS_MAX_ENTRY_SIZE (DBS_BLOCK_SIZE - (2048)) /* 14 k */ michael@0: #define DBS_CACHE_SIZE DBS_BLOCK_SIZE*8 michael@0: #define ROUNDDIV(x,y) (x+(y-1))/y michael@0: #define BLOB_HEAD_LEN 4 michael@0: #define BLOB_LENGTH_START BLOB_HEAD_LEN michael@0: #define BLOB_LENGTH_LEN 4 michael@0: #define BLOB_NAME_START BLOB_LENGTH_START+BLOB_LENGTH_LEN michael@0: #define BLOB_NAME_LEN 1+ROUNDDIV(SHA1_LENGTH,3)*4+1 michael@0: #define BLOB_BUF_LEN BLOB_HEAD_LEN+BLOB_LENGTH_LEN+BLOB_NAME_LEN michael@0: michael@0: /* a Shim data structure. This data structure has a db built into it. */ michael@0: typedef struct DBSStr DBS; michael@0: michael@0: struct DBSStr { michael@0: DB db; michael@0: char *blobdir; michael@0: int mode; michael@0: PRBool readOnly; michael@0: PRFileMap *dbs_mapfile; michael@0: unsigned char *dbs_addr; michael@0: PRUint32 dbs_len; michael@0: char staticBlobArea[BLOB_BUF_LEN]; michael@0: }; michael@0: michael@0: michael@0: michael@0: /* michael@0: * return true if the Datablock contains a blobtype michael@0: */ michael@0: static PRBool michael@0: dbs_IsBlob(DBT *blobData) michael@0: { michael@0: unsigned char *addr = (unsigned char *)blobData->data; michael@0: if (blobData->size < BLOB_BUF_LEN) { michael@0: return PR_FALSE; michael@0: } michael@0: return addr && ((certDBEntryType) addr[1] == certDBEntryTypeBlob); michael@0: } michael@0: michael@0: /* michael@0: * extract the filename in the blob of the real data set. michael@0: * This value is not malloced (does not need to be freed by the caller. michael@0: */ michael@0: static const char * michael@0: dbs_getBlobFileName(DBT *blobData) michael@0: { michael@0: char *addr = (char *)blobData->data; michael@0: michael@0: return &addr[BLOB_NAME_START]; michael@0: } michael@0: michael@0: /* michael@0: * extract the size of the actual blob from the blob record michael@0: */ michael@0: static PRUint32 michael@0: dbs_getBlobSize(DBT *blobData) michael@0: { michael@0: unsigned char *addr = (unsigned char *)blobData->data; michael@0: michael@0: return (PRUint32)(addr[BLOB_LENGTH_START+3] << 24) | michael@0: (addr[BLOB_LENGTH_START+2] << 16) | michael@0: (addr[BLOB_LENGTH_START+1] << 8) | michael@0: addr[BLOB_LENGTH_START]; michael@0: } michael@0: michael@0: michael@0: /* We are using base64 data for the filename, but base64 data can include a michael@0: * '/' which is interpreted as a path separator on many platforms. Replace it michael@0: * with an inocuous '-'. We don't need to convert back because we never actual michael@0: * decode the filename. michael@0: */ michael@0: michael@0: static void michael@0: dbs_replaceSlash(char *cp, int len) michael@0: { michael@0: while (len--) { michael@0: if (*cp == '/') *cp = '-'; michael@0: cp++; michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * create a blob record from a key, data and return it in blobData. michael@0: * NOTE: The data element is static data (keeping with the dbm model). michael@0: */ michael@0: static void michael@0: dbs_mkBlob(DBS *dbsp,const DBT *key, const DBT *data, DBT *blobData) michael@0: { michael@0: unsigned char sha1_data[SHA1_LENGTH]; michael@0: char *b = dbsp->staticBlobArea; michael@0: PRUint32 length = data->size; michael@0: SECItem sha1Item; michael@0: michael@0: b[0] = CERT_DB_FILE_VERSION; /* certdb version number */ michael@0: b[1] = (char) certDBEntryTypeBlob; /* type */ michael@0: b[2] = 0; /* flags */ michael@0: b[3] = 0; /* reserved */ michael@0: b[BLOB_LENGTH_START] = length & 0xff; michael@0: b[BLOB_LENGTH_START+1] = (length >> 8) & 0xff; michael@0: b[BLOB_LENGTH_START+2] = (length >> 16) & 0xff; michael@0: b[BLOB_LENGTH_START+3] = (length >> 24) & 0xff; michael@0: sha1Item.data = sha1_data; michael@0: sha1Item.len = SHA1_LENGTH; michael@0: SHA1_HashBuf(sha1_data,key->data,key->size); michael@0: b[BLOB_NAME_START]='b'; /* Make sure we start with a alpha */ michael@0: NSSBase64_EncodeItem(NULL,&b[BLOB_NAME_START+1],BLOB_NAME_LEN-1,&sha1Item); michael@0: b[BLOB_BUF_LEN-1] = 0; michael@0: dbs_replaceSlash(&b[BLOB_NAME_START+1],BLOB_NAME_LEN-1); michael@0: blobData->data = b; michael@0: blobData->size = BLOB_BUF_LEN; michael@0: return; michael@0: } michael@0: michael@0: michael@0: /* michael@0: * construct a path to the actual blob. The string returned must be michael@0: * freed by the caller with PR_smprintf_free. michael@0: * michael@0: * Note: this file does lots of consistancy checks on the DBT. The michael@0: * routines that call this depend on these checks, so they don't worry michael@0: * about them (success of this routine implies a good blobdata record). michael@0: */ michael@0: static char * michael@0: dbs_getBlobFilePath(char *blobdir,DBT *blobData) michael@0: { michael@0: const char *name; michael@0: michael@0: if (blobdir == NULL) { michael@0: PR_SetError(SEC_ERROR_BAD_DATABASE,0); michael@0: return NULL; michael@0: } michael@0: if (!dbs_IsBlob(blobData)) { michael@0: PR_SetError(SEC_ERROR_BAD_DATABASE,0); michael@0: return NULL; michael@0: } michael@0: name = dbs_getBlobFileName(blobData); michael@0: if (!name || *name == 0) { michael@0: PR_SetError(SEC_ERROR_BAD_DATABASE,0); michael@0: return NULL; michael@0: } michael@0: return PR_smprintf("%s" PATH_SEPARATOR "%s", blobdir, name); michael@0: } michael@0: michael@0: /* michael@0: * Delete a blob file pointed to by the blob record. michael@0: */ michael@0: static void michael@0: dbs_removeBlob(DBS *dbsp, DBT *blobData) michael@0: { michael@0: char *file; michael@0: michael@0: file = dbs_getBlobFilePath(dbsp->blobdir, blobData); michael@0: if (!file) { michael@0: return; michael@0: } michael@0: PR_Delete(file); michael@0: PR_smprintf_free(file); michael@0: } michael@0: michael@0: /* michael@0: * Directory modes are slightly different, the 'x' bit needs to be on to michael@0: * access them. Copy all the read bits to 'x' bits michael@0: */ michael@0: static int michael@0: dbs_DirMode(int mode) michael@0: { michael@0: int x_bits = (mode >> 2) & 0111; michael@0: return mode | x_bits; michael@0: } michael@0: michael@0: /* michael@0: * write a data blob to it's file. blobdData is the blob record that will be michael@0: * stored in the database. data is the actual data to go out on disk. michael@0: */ michael@0: static int michael@0: dbs_writeBlob(DBS *dbsp, int mode, DBT *blobData, const DBT *data) michael@0: { michael@0: char *file = NULL; michael@0: PRFileDesc *filed; michael@0: PRStatus status; michael@0: int len; michael@0: int error = 0; michael@0: michael@0: file = dbs_getBlobFilePath(dbsp->blobdir, blobData); michael@0: if (!file) { michael@0: goto loser; michael@0: } michael@0: if (PR_Access(dbsp->blobdir, PR_ACCESS_EXISTS) != PR_SUCCESS) { michael@0: status = PR_MkDir(dbsp->blobdir,dbs_DirMode(mode)); michael@0: if (status != PR_SUCCESS) { michael@0: goto loser; michael@0: } michael@0: } michael@0: filed = PR_OpenFile(file,PR_CREATE_FILE|PR_TRUNCATE|PR_WRONLY, mode); michael@0: if (filed == NULL) { michael@0: error = PR_GetError(); michael@0: goto loser; michael@0: } michael@0: len = PR_Write(filed,data->data,data->size); michael@0: error = PR_GetError(); michael@0: PR_Close(filed); michael@0: if (len < (int)data->size) { michael@0: goto loser; michael@0: } michael@0: PR_smprintf_free(file); michael@0: return 0; michael@0: michael@0: loser: michael@0: if (file) { michael@0: PR_Delete(file); michael@0: PR_smprintf_free(file); michael@0: } michael@0: /* don't let close or delete reset the error */ michael@0: PR_SetError(error,0); michael@0: return -1; michael@0: } michael@0: michael@0: michael@0: /* michael@0: * we need to keep a address map in memory between calls to DBM. michael@0: * remember what we have mapped can close it when we get another dbm michael@0: * call. michael@0: * michael@0: * NOTE: Not all platforms support mapped files. This code is designed to michael@0: * detect this at runtime. If map files aren't supported the OS will indicate michael@0: * this by failing the PR_Memmap call. In this case we emulate mapped files michael@0: * by just reading in the file into regular memory. We signal this state by michael@0: * making dbs_mapfile NULL and dbs_addr non-NULL. michael@0: */ michael@0: michael@0: static void michael@0: dbs_freemap(DBS *dbsp) michael@0: { michael@0: if (dbsp->dbs_mapfile) { michael@0: PR_MemUnmap(dbsp->dbs_addr,dbsp->dbs_len); michael@0: PR_CloseFileMap(dbsp->dbs_mapfile); michael@0: dbsp->dbs_mapfile = NULL; michael@0: dbsp->dbs_addr = NULL; michael@0: dbsp->dbs_len = 0; michael@0: } else if (dbsp->dbs_addr) { michael@0: PORT_Free(dbsp->dbs_addr); michael@0: dbsp->dbs_addr = NULL; michael@0: dbsp->dbs_len = 0; michael@0: } michael@0: return; michael@0: } michael@0: michael@0: static void michael@0: dbs_setmap(DBS *dbsp, PRFileMap *mapfile, unsigned char *addr, PRUint32 len) michael@0: { michael@0: dbsp->dbs_mapfile = mapfile; michael@0: dbsp->dbs_addr = addr; michael@0: dbsp->dbs_len = len; michael@0: } michael@0: michael@0: /* michael@0: * platforms that cannot map the file need to read it into a temp buffer. michael@0: */ michael@0: static unsigned char * michael@0: dbs_EmulateMap(PRFileDesc *filed, int len) michael@0: { michael@0: unsigned char *addr; michael@0: PRInt32 dataRead; michael@0: michael@0: addr = PORT_Alloc(len); michael@0: if (addr == NULL) { michael@0: return NULL; michael@0: } michael@0: michael@0: dataRead = PR_Read(filed,addr,len); michael@0: if (dataRead != len) { michael@0: PORT_Free(addr); michael@0: if (dataRead > 0) { michael@0: /* PR_Read didn't set an error, we need to */ michael@0: PR_SetError(SEC_ERROR_BAD_DATABASE,0); michael@0: } michael@0: return NULL; michael@0: } michael@0: michael@0: return addr; michael@0: } michael@0: michael@0: michael@0: /* michael@0: * pull a database record off the disk michael@0: * data points to the blob record on input and the real record (if we could michael@0: * read it) on output. if there is an error data is not modified. michael@0: */ michael@0: static int michael@0: dbs_readBlob(DBS *dbsp, DBT *data) michael@0: { michael@0: char *file = NULL; michael@0: PRFileDesc *filed = NULL; michael@0: PRFileMap *mapfile = NULL; michael@0: unsigned char *addr = NULL; michael@0: int error; michael@0: int len = -1; michael@0: michael@0: file = dbs_getBlobFilePath(dbsp->blobdir, data); michael@0: if (!file) { michael@0: goto loser; michael@0: } michael@0: filed = PR_OpenFile(file,PR_RDONLY,0); michael@0: PR_smprintf_free(file); file = NULL; michael@0: if (filed == NULL) { michael@0: goto loser; michael@0: } michael@0: michael@0: len = dbs_getBlobSize(data); michael@0: mapfile = PR_CreateFileMap(filed, len, PR_PROT_READONLY); michael@0: if (mapfile == NULL) { michael@0: /* USE PR_GetError instead of PORT_GetError here michael@0: * because we are getting the error from PR_xxx michael@0: * function */ michael@0: if (PR_GetError() != PR_NOT_IMPLEMENTED_ERROR) { michael@0: goto loser; michael@0: } michael@0: addr = dbs_EmulateMap(filed, len); michael@0: } else { michael@0: addr = PR_MemMap(mapfile, 0, len); michael@0: } michael@0: if (addr == NULL) { michael@0: goto loser; michael@0: } michael@0: PR_Close(filed); michael@0: dbs_setmap(dbsp,mapfile,addr,len); michael@0: michael@0: data->data = addr; michael@0: data->size = len; michael@0: return 0; michael@0: michael@0: loser: michael@0: /* preserve the error code */ michael@0: error = PR_GetError(); michael@0: if (mapfile) { michael@0: PR_CloseFileMap(mapfile); michael@0: } michael@0: if (filed) { michael@0: PR_Close(filed); michael@0: } michael@0: PR_SetError(error,0); michael@0: return -1; michael@0: } michael@0: michael@0: /* michael@0: * actual DBM shims michael@0: */ michael@0: static int michael@0: dbs_get(const DB *dbs, const DBT *key, DBT *data, unsigned int flags) michael@0: { michael@0: int ret; michael@0: DBS *dbsp = (DBS *)dbs; michael@0: DB *db = (DB *)dbs->internal; michael@0: michael@0: michael@0: dbs_freemap(dbsp); michael@0: michael@0: ret = (* db->get)(db, key, data, flags); michael@0: if ((ret == 0) && dbs_IsBlob(data)) { michael@0: ret = dbs_readBlob(dbsp,data); michael@0: } michael@0: michael@0: return(ret); michael@0: } michael@0: michael@0: static int michael@0: dbs_put(const DB *dbs, DBT *key, const DBT *data, unsigned int flags) michael@0: { michael@0: DBT blob; michael@0: int ret = 0; michael@0: DBS *dbsp = (DBS *)dbs; michael@0: DB *db = (DB *)dbs->internal; michael@0: michael@0: dbs_freemap(dbsp); michael@0: michael@0: /* If the db is readonly, just pass the data down to rdb and let it fail */ michael@0: if (!dbsp->readOnly) { michael@0: DBT oldData; michael@0: int ret1; michael@0: michael@0: /* make sure the current record is deleted if it's a blob */ michael@0: ret1 = (*db->get)(db,key,&oldData,0); michael@0: if ((ret1 == 0) && flags == R_NOOVERWRITE) { michael@0: /* let DBM return the error to maintain consistancy */ michael@0: return (* db->put)(db, key, data, flags); michael@0: } michael@0: if ((ret1 == 0) && dbs_IsBlob(&oldData)) { michael@0: dbs_removeBlob(dbsp, &oldData); michael@0: } michael@0: michael@0: if (data->size > DBS_MAX_ENTRY_SIZE) { michael@0: dbs_mkBlob(dbsp,key,data,&blob); michael@0: ret = dbs_writeBlob(dbsp, dbsp->mode, &blob, data); michael@0: data = &blob; michael@0: } michael@0: } michael@0: michael@0: if (ret == 0) { michael@0: ret = (* db->put)(db, key, data, flags); michael@0: } michael@0: return(ret); michael@0: } michael@0: michael@0: static int michael@0: dbs_sync(const DB *dbs, unsigned int flags) michael@0: { michael@0: DB *db = (DB *)dbs->internal; michael@0: DBS *dbsp = (DBS *)dbs; michael@0: michael@0: dbs_freemap(dbsp); michael@0: michael@0: return (* db->sync)(db, flags); michael@0: } michael@0: michael@0: static int michael@0: dbs_del(const DB *dbs, const DBT *key, unsigned int flags) michael@0: { michael@0: int ret; michael@0: DBS *dbsp = (DBS *)dbs; michael@0: DB *db = (DB *)dbs->internal; michael@0: michael@0: dbs_freemap(dbsp); michael@0: michael@0: if (!dbsp->readOnly) { michael@0: DBT oldData; michael@0: ret = (*db->get)(db,key,&oldData,0); michael@0: if ((ret == 0) && dbs_IsBlob(&oldData)) { michael@0: dbs_removeBlob(dbsp,&oldData); michael@0: } michael@0: } michael@0: michael@0: return (* db->del)(db, key, flags); michael@0: } michael@0: michael@0: static int michael@0: dbs_seq(const DB *dbs, DBT *key, DBT *data, unsigned int flags) michael@0: { michael@0: int ret; michael@0: DBS *dbsp = (DBS *)dbs; michael@0: DB *db = (DB *)dbs->internal; michael@0: michael@0: dbs_freemap(dbsp); michael@0: michael@0: ret = (* db->seq)(db, key, data, flags); michael@0: if ((ret == 0) && dbs_IsBlob(data)) { michael@0: /* don't return a blob read as an error so traversals keep going */ michael@0: (void) dbs_readBlob(dbsp,data); michael@0: } michael@0: michael@0: return(ret); michael@0: } michael@0: michael@0: static int michael@0: dbs_close(DB *dbs) michael@0: { michael@0: DBS *dbsp = (DBS *)dbs; michael@0: DB *db = (DB *)dbs->internal; michael@0: int ret; michael@0: michael@0: dbs_freemap(dbsp); michael@0: ret = (* db->close)(db); michael@0: PORT_Free(dbsp->blobdir); michael@0: PORT_Free(dbsp); michael@0: return ret; michael@0: } michael@0: michael@0: static int michael@0: dbs_fd(const DB *dbs) michael@0: { michael@0: DB *db = (DB *)dbs->internal; michael@0: michael@0: return (* db->fd)(db); michael@0: } michael@0: michael@0: /* michael@0: * the naming convention we use is michael@0: * change the .xxx into .dir. (for nss it's always .db); michael@0: * if no .extension exists or is equal to .dir, add a .dir michael@0: * the returned data must be freed. michael@0: */ michael@0: #define DIRSUFFIX ".dir" michael@0: static char * michael@0: dbs_mkBlobDirName(const char *dbname) michael@0: { michael@0: int dbname_len = PORT_Strlen(dbname); michael@0: int dbname_end = dbname_len; michael@0: const char *cp; michael@0: char *blobDir = NULL; michael@0: michael@0: /* scan back from the end looking for either a directory separator, a '.', michael@0: * or the end of the string. NOTE: Windows should check for both separators michael@0: * here. For now this is safe because we know NSS always uses a '.' michael@0: */ michael@0: for (cp = &dbname[dbname_len]; michael@0: (cp > dbname) && (*cp != '.') && (*cp != *PATH_SEPARATOR) ; michael@0: cp--) michael@0: /* Empty */ ; michael@0: if (*cp == '.') { michael@0: dbname_end = cp - dbname; michael@0: if (PORT_Strcmp(cp,DIRSUFFIX) == 0) { michael@0: dbname_end = dbname_len; michael@0: } michael@0: } michael@0: blobDir = PORT_ZAlloc(dbname_end+sizeof(DIRSUFFIX)); michael@0: if (blobDir == NULL) { michael@0: return NULL; michael@0: } michael@0: PORT_Memcpy(blobDir,dbname,dbname_end); michael@0: PORT_Memcpy(&blobDir[dbname_end],DIRSUFFIX,sizeof(DIRSUFFIX)); michael@0: return blobDir; michael@0: } michael@0: michael@0: #define DBM_DEFAULT 0 michael@0: static const HASHINFO dbs_hashInfo = { michael@0: DBS_BLOCK_SIZE, /* bucket size, must be greater than = to michael@0: * or maximum entry size (+ header) michael@0: * we allow before blobing */ michael@0: DBM_DEFAULT, /* Fill Factor */ michael@0: DBM_DEFAULT, /* number of elements */ michael@0: DBS_CACHE_SIZE, /* cache size */ michael@0: DBM_DEFAULT, /* hash function */ michael@0: DBM_DEFAULT, /* byte order */ michael@0: }; michael@0: michael@0: /* michael@0: * the open function. NOTE: this is the only exposed function in this file. michael@0: * everything else is called through the function table pointer. michael@0: */ michael@0: DB * michael@0: dbsopen(const char *dbname, int flags, int mode, DBTYPE type, michael@0: const void *userData) michael@0: { michael@0: DB *db = NULL,*dbs = NULL; michael@0: DBS *dbsp = NULL; michael@0: michael@0: /* NOTE: we are overriding userData with dbs_hashInfo. since all known michael@0: * callers pass 0, this is ok, otherwise we should merge the two */ michael@0: michael@0: dbsp = (DBS *)PORT_ZAlloc(sizeof(DBS)); michael@0: if (!dbsp) { michael@0: return NULL; michael@0: } michael@0: dbs = &dbsp->db; michael@0: michael@0: dbsp->blobdir=dbs_mkBlobDirName(dbname); michael@0: if (dbsp->blobdir == NULL) { michael@0: goto loser; michael@0: } michael@0: dbsp->mode = mode; michael@0: dbsp->readOnly = (PRBool)(flags == NO_RDONLY); michael@0: dbsp->dbs_mapfile = NULL; michael@0: dbsp->dbs_addr = NULL; michael@0: dbsp->dbs_len = 0; michael@0: michael@0: /* the real dbm call */ michael@0: db = dbopen(dbname, flags, mode, type, &dbs_hashInfo); michael@0: if (db == NULL) { michael@0: goto loser; michael@0: } michael@0: dbs->internal = (void *) db; michael@0: dbs->type = type; michael@0: dbs->close = dbs_close; michael@0: dbs->get = dbs_get; michael@0: dbs->del = dbs_del; michael@0: dbs->put = dbs_put; michael@0: dbs->seq = dbs_seq; michael@0: dbs->sync = dbs_sync; michael@0: dbs->fd = dbs_fd; michael@0: michael@0: return dbs; michael@0: loser: michael@0: if (db) { michael@0: (*db->close)(db); michael@0: } michael@0: if (dbsp) { michael@0: if (dbsp->blobdir) { michael@0: PORT_Free(dbsp->blobdir); michael@0: } michael@0: PORT_Free(dbsp); michael@0: } michael@0: return NULL; michael@0: }