|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /* |
|
6 * Berkeley DB 1.85 Shim code to handle blobs. |
|
7 */ |
|
8 #include "mcom_db.h" |
|
9 #include "secitem.h" |
|
10 #include "nssb64.h" |
|
11 #include "blapi.h" |
|
12 #include "secerr.h" |
|
13 |
|
14 #include "lgdb.h" |
|
15 |
|
16 /* |
|
17 * Blob block: |
|
18 * Byte 0 CERTDB Version -+ -+ |
|
19 * Byte 1 certDBEntryTypeBlob | BLOB_HEAD_LEN | |
|
20 * Byte 2 flags (always '0'); | | |
|
21 * Byte 3 reserved (always '0'); -+ | |
|
22 * Byte 4 LSB length | <--BLOB_LENGTH_START | BLOB_BUF_LEN |
|
23 * Byte 5 . | | |
|
24 * Byte 6 . | BLOB_LENGTH_LEN | |
|
25 * Byte 7 MSB length | | |
|
26 * Byte 8 blob_filename -+ -+ <-- BLOB_NAME_START | |
|
27 * Byte 9 . | BLOB_NAME_LEN | |
|
28 * . . | | |
|
29 * Byte 37 . -+ -+ |
|
30 */ |
|
31 #define DBS_BLOCK_SIZE (16*1024) /* 16 k */ |
|
32 #define DBS_MAX_ENTRY_SIZE (DBS_BLOCK_SIZE - (2048)) /* 14 k */ |
|
33 #define DBS_CACHE_SIZE DBS_BLOCK_SIZE*8 |
|
34 #define ROUNDDIV(x,y) (x+(y-1))/y |
|
35 #define BLOB_HEAD_LEN 4 |
|
36 #define BLOB_LENGTH_START BLOB_HEAD_LEN |
|
37 #define BLOB_LENGTH_LEN 4 |
|
38 #define BLOB_NAME_START BLOB_LENGTH_START+BLOB_LENGTH_LEN |
|
39 #define BLOB_NAME_LEN 1+ROUNDDIV(SHA1_LENGTH,3)*4+1 |
|
40 #define BLOB_BUF_LEN BLOB_HEAD_LEN+BLOB_LENGTH_LEN+BLOB_NAME_LEN |
|
41 |
|
42 /* a Shim data structure. This data structure has a db built into it. */ |
|
43 typedef struct DBSStr DBS; |
|
44 |
|
45 struct DBSStr { |
|
46 DB db; |
|
47 char *blobdir; |
|
48 int mode; |
|
49 PRBool readOnly; |
|
50 PRFileMap *dbs_mapfile; |
|
51 unsigned char *dbs_addr; |
|
52 PRUint32 dbs_len; |
|
53 char staticBlobArea[BLOB_BUF_LEN]; |
|
54 }; |
|
55 |
|
56 |
|
57 |
|
58 /* |
|
59 * return true if the Datablock contains a blobtype |
|
60 */ |
|
61 static PRBool |
|
62 dbs_IsBlob(DBT *blobData) |
|
63 { |
|
64 unsigned char *addr = (unsigned char *)blobData->data; |
|
65 if (blobData->size < BLOB_BUF_LEN) { |
|
66 return PR_FALSE; |
|
67 } |
|
68 return addr && ((certDBEntryType) addr[1] == certDBEntryTypeBlob); |
|
69 } |
|
70 |
|
71 /* |
|
72 * extract the filename in the blob of the real data set. |
|
73 * This value is not malloced (does not need to be freed by the caller. |
|
74 */ |
|
75 static const char * |
|
76 dbs_getBlobFileName(DBT *blobData) |
|
77 { |
|
78 char *addr = (char *)blobData->data; |
|
79 |
|
80 return &addr[BLOB_NAME_START]; |
|
81 } |
|
82 |
|
83 /* |
|
84 * extract the size of the actual blob from the blob record |
|
85 */ |
|
86 static PRUint32 |
|
87 dbs_getBlobSize(DBT *blobData) |
|
88 { |
|
89 unsigned char *addr = (unsigned char *)blobData->data; |
|
90 |
|
91 return (PRUint32)(addr[BLOB_LENGTH_START+3] << 24) | |
|
92 (addr[BLOB_LENGTH_START+2] << 16) | |
|
93 (addr[BLOB_LENGTH_START+1] << 8) | |
|
94 addr[BLOB_LENGTH_START]; |
|
95 } |
|
96 |
|
97 |
|
98 /* We are using base64 data for the filename, but base64 data can include a |
|
99 * '/' which is interpreted as a path separator on many platforms. Replace it |
|
100 * with an inocuous '-'. We don't need to convert back because we never actual |
|
101 * decode the filename. |
|
102 */ |
|
103 |
|
104 static void |
|
105 dbs_replaceSlash(char *cp, int len) |
|
106 { |
|
107 while (len--) { |
|
108 if (*cp == '/') *cp = '-'; |
|
109 cp++; |
|
110 } |
|
111 } |
|
112 |
|
113 /* |
|
114 * create a blob record from a key, data and return it in blobData. |
|
115 * NOTE: The data element is static data (keeping with the dbm model). |
|
116 */ |
|
117 static void |
|
118 dbs_mkBlob(DBS *dbsp,const DBT *key, const DBT *data, DBT *blobData) |
|
119 { |
|
120 unsigned char sha1_data[SHA1_LENGTH]; |
|
121 char *b = dbsp->staticBlobArea; |
|
122 PRUint32 length = data->size; |
|
123 SECItem sha1Item; |
|
124 |
|
125 b[0] = CERT_DB_FILE_VERSION; /* certdb version number */ |
|
126 b[1] = (char) certDBEntryTypeBlob; /* type */ |
|
127 b[2] = 0; /* flags */ |
|
128 b[3] = 0; /* reserved */ |
|
129 b[BLOB_LENGTH_START] = length & 0xff; |
|
130 b[BLOB_LENGTH_START+1] = (length >> 8) & 0xff; |
|
131 b[BLOB_LENGTH_START+2] = (length >> 16) & 0xff; |
|
132 b[BLOB_LENGTH_START+3] = (length >> 24) & 0xff; |
|
133 sha1Item.data = sha1_data; |
|
134 sha1Item.len = SHA1_LENGTH; |
|
135 SHA1_HashBuf(sha1_data,key->data,key->size); |
|
136 b[BLOB_NAME_START]='b'; /* Make sure we start with a alpha */ |
|
137 NSSBase64_EncodeItem(NULL,&b[BLOB_NAME_START+1],BLOB_NAME_LEN-1,&sha1Item); |
|
138 b[BLOB_BUF_LEN-1] = 0; |
|
139 dbs_replaceSlash(&b[BLOB_NAME_START+1],BLOB_NAME_LEN-1); |
|
140 blobData->data = b; |
|
141 blobData->size = BLOB_BUF_LEN; |
|
142 return; |
|
143 } |
|
144 |
|
145 |
|
146 /* |
|
147 * construct a path to the actual blob. The string returned must be |
|
148 * freed by the caller with PR_smprintf_free. |
|
149 * |
|
150 * Note: this file does lots of consistancy checks on the DBT. The |
|
151 * routines that call this depend on these checks, so they don't worry |
|
152 * about them (success of this routine implies a good blobdata record). |
|
153 */ |
|
154 static char * |
|
155 dbs_getBlobFilePath(char *blobdir,DBT *blobData) |
|
156 { |
|
157 const char *name; |
|
158 |
|
159 if (blobdir == NULL) { |
|
160 PR_SetError(SEC_ERROR_BAD_DATABASE,0); |
|
161 return NULL; |
|
162 } |
|
163 if (!dbs_IsBlob(blobData)) { |
|
164 PR_SetError(SEC_ERROR_BAD_DATABASE,0); |
|
165 return NULL; |
|
166 } |
|
167 name = dbs_getBlobFileName(blobData); |
|
168 if (!name || *name == 0) { |
|
169 PR_SetError(SEC_ERROR_BAD_DATABASE,0); |
|
170 return NULL; |
|
171 } |
|
172 return PR_smprintf("%s" PATH_SEPARATOR "%s", blobdir, name); |
|
173 } |
|
174 |
|
175 /* |
|
176 * Delete a blob file pointed to by the blob record. |
|
177 */ |
|
178 static void |
|
179 dbs_removeBlob(DBS *dbsp, DBT *blobData) |
|
180 { |
|
181 char *file; |
|
182 |
|
183 file = dbs_getBlobFilePath(dbsp->blobdir, blobData); |
|
184 if (!file) { |
|
185 return; |
|
186 } |
|
187 PR_Delete(file); |
|
188 PR_smprintf_free(file); |
|
189 } |
|
190 |
|
191 /* |
|
192 * Directory modes are slightly different, the 'x' bit needs to be on to |
|
193 * access them. Copy all the read bits to 'x' bits |
|
194 */ |
|
195 static int |
|
196 dbs_DirMode(int mode) |
|
197 { |
|
198 int x_bits = (mode >> 2) & 0111; |
|
199 return mode | x_bits; |
|
200 } |
|
201 |
|
202 /* |
|
203 * write a data blob to it's file. blobdData is the blob record that will be |
|
204 * stored in the database. data is the actual data to go out on disk. |
|
205 */ |
|
206 static int |
|
207 dbs_writeBlob(DBS *dbsp, int mode, DBT *blobData, const DBT *data) |
|
208 { |
|
209 char *file = NULL; |
|
210 PRFileDesc *filed; |
|
211 PRStatus status; |
|
212 int len; |
|
213 int error = 0; |
|
214 |
|
215 file = dbs_getBlobFilePath(dbsp->blobdir, blobData); |
|
216 if (!file) { |
|
217 goto loser; |
|
218 } |
|
219 if (PR_Access(dbsp->blobdir, PR_ACCESS_EXISTS) != PR_SUCCESS) { |
|
220 status = PR_MkDir(dbsp->blobdir,dbs_DirMode(mode)); |
|
221 if (status != PR_SUCCESS) { |
|
222 goto loser; |
|
223 } |
|
224 } |
|
225 filed = PR_OpenFile(file,PR_CREATE_FILE|PR_TRUNCATE|PR_WRONLY, mode); |
|
226 if (filed == NULL) { |
|
227 error = PR_GetError(); |
|
228 goto loser; |
|
229 } |
|
230 len = PR_Write(filed,data->data,data->size); |
|
231 error = PR_GetError(); |
|
232 PR_Close(filed); |
|
233 if (len < (int)data->size) { |
|
234 goto loser; |
|
235 } |
|
236 PR_smprintf_free(file); |
|
237 return 0; |
|
238 |
|
239 loser: |
|
240 if (file) { |
|
241 PR_Delete(file); |
|
242 PR_smprintf_free(file); |
|
243 } |
|
244 /* don't let close or delete reset the error */ |
|
245 PR_SetError(error,0); |
|
246 return -1; |
|
247 } |
|
248 |
|
249 |
|
250 /* |
|
251 * we need to keep a address map in memory between calls to DBM. |
|
252 * remember what we have mapped can close it when we get another dbm |
|
253 * call. |
|
254 * |
|
255 * NOTE: Not all platforms support mapped files. This code is designed to |
|
256 * detect this at runtime. If map files aren't supported the OS will indicate |
|
257 * this by failing the PR_Memmap call. In this case we emulate mapped files |
|
258 * by just reading in the file into regular memory. We signal this state by |
|
259 * making dbs_mapfile NULL and dbs_addr non-NULL. |
|
260 */ |
|
261 |
|
262 static void |
|
263 dbs_freemap(DBS *dbsp) |
|
264 { |
|
265 if (dbsp->dbs_mapfile) { |
|
266 PR_MemUnmap(dbsp->dbs_addr,dbsp->dbs_len); |
|
267 PR_CloseFileMap(dbsp->dbs_mapfile); |
|
268 dbsp->dbs_mapfile = NULL; |
|
269 dbsp->dbs_addr = NULL; |
|
270 dbsp->dbs_len = 0; |
|
271 } else if (dbsp->dbs_addr) { |
|
272 PORT_Free(dbsp->dbs_addr); |
|
273 dbsp->dbs_addr = NULL; |
|
274 dbsp->dbs_len = 0; |
|
275 } |
|
276 return; |
|
277 } |
|
278 |
|
279 static void |
|
280 dbs_setmap(DBS *dbsp, PRFileMap *mapfile, unsigned char *addr, PRUint32 len) |
|
281 { |
|
282 dbsp->dbs_mapfile = mapfile; |
|
283 dbsp->dbs_addr = addr; |
|
284 dbsp->dbs_len = len; |
|
285 } |
|
286 |
|
287 /* |
|
288 * platforms that cannot map the file need to read it into a temp buffer. |
|
289 */ |
|
290 static unsigned char * |
|
291 dbs_EmulateMap(PRFileDesc *filed, int len) |
|
292 { |
|
293 unsigned char *addr; |
|
294 PRInt32 dataRead; |
|
295 |
|
296 addr = PORT_Alloc(len); |
|
297 if (addr == NULL) { |
|
298 return NULL; |
|
299 } |
|
300 |
|
301 dataRead = PR_Read(filed,addr,len); |
|
302 if (dataRead != len) { |
|
303 PORT_Free(addr); |
|
304 if (dataRead > 0) { |
|
305 /* PR_Read didn't set an error, we need to */ |
|
306 PR_SetError(SEC_ERROR_BAD_DATABASE,0); |
|
307 } |
|
308 return NULL; |
|
309 } |
|
310 |
|
311 return addr; |
|
312 } |
|
313 |
|
314 |
|
315 /* |
|
316 * pull a database record off the disk |
|
317 * data points to the blob record on input and the real record (if we could |
|
318 * read it) on output. if there is an error data is not modified. |
|
319 */ |
|
320 static int |
|
321 dbs_readBlob(DBS *dbsp, DBT *data) |
|
322 { |
|
323 char *file = NULL; |
|
324 PRFileDesc *filed = NULL; |
|
325 PRFileMap *mapfile = NULL; |
|
326 unsigned char *addr = NULL; |
|
327 int error; |
|
328 int len = -1; |
|
329 |
|
330 file = dbs_getBlobFilePath(dbsp->blobdir, data); |
|
331 if (!file) { |
|
332 goto loser; |
|
333 } |
|
334 filed = PR_OpenFile(file,PR_RDONLY,0); |
|
335 PR_smprintf_free(file); file = NULL; |
|
336 if (filed == NULL) { |
|
337 goto loser; |
|
338 } |
|
339 |
|
340 len = dbs_getBlobSize(data); |
|
341 mapfile = PR_CreateFileMap(filed, len, PR_PROT_READONLY); |
|
342 if (mapfile == NULL) { |
|
343 /* USE PR_GetError instead of PORT_GetError here |
|
344 * because we are getting the error from PR_xxx |
|
345 * function */ |
|
346 if (PR_GetError() != PR_NOT_IMPLEMENTED_ERROR) { |
|
347 goto loser; |
|
348 } |
|
349 addr = dbs_EmulateMap(filed, len); |
|
350 } else { |
|
351 addr = PR_MemMap(mapfile, 0, len); |
|
352 } |
|
353 if (addr == NULL) { |
|
354 goto loser; |
|
355 } |
|
356 PR_Close(filed); |
|
357 dbs_setmap(dbsp,mapfile,addr,len); |
|
358 |
|
359 data->data = addr; |
|
360 data->size = len; |
|
361 return 0; |
|
362 |
|
363 loser: |
|
364 /* preserve the error code */ |
|
365 error = PR_GetError(); |
|
366 if (mapfile) { |
|
367 PR_CloseFileMap(mapfile); |
|
368 } |
|
369 if (filed) { |
|
370 PR_Close(filed); |
|
371 } |
|
372 PR_SetError(error,0); |
|
373 return -1; |
|
374 } |
|
375 |
|
376 /* |
|
377 * actual DBM shims |
|
378 */ |
|
379 static int |
|
380 dbs_get(const DB *dbs, const DBT *key, DBT *data, unsigned int flags) |
|
381 { |
|
382 int ret; |
|
383 DBS *dbsp = (DBS *)dbs; |
|
384 DB *db = (DB *)dbs->internal; |
|
385 |
|
386 |
|
387 dbs_freemap(dbsp); |
|
388 |
|
389 ret = (* db->get)(db, key, data, flags); |
|
390 if ((ret == 0) && dbs_IsBlob(data)) { |
|
391 ret = dbs_readBlob(dbsp,data); |
|
392 } |
|
393 |
|
394 return(ret); |
|
395 } |
|
396 |
|
397 static int |
|
398 dbs_put(const DB *dbs, DBT *key, const DBT *data, unsigned int flags) |
|
399 { |
|
400 DBT blob; |
|
401 int ret = 0; |
|
402 DBS *dbsp = (DBS *)dbs; |
|
403 DB *db = (DB *)dbs->internal; |
|
404 |
|
405 dbs_freemap(dbsp); |
|
406 |
|
407 /* If the db is readonly, just pass the data down to rdb and let it fail */ |
|
408 if (!dbsp->readOnly) { |
|
409 DBT oldData; |
|
410 int ret1; |
|
411 |
|
412 /* make sure the current record is deleted if it's a blob */ |
|
413 ret1 = (*db->get)(db,key,&oldData,0); |
|
414 if ((ret1 == 0) && flags == R_NOOVERWRITE) { |
|
415 /* let DBM return the error to maintain consistancy */ |
|
416 return (* db->put)(db, key, data, flags); |
|
417 } |
|
418 if ((ret1 == 0) && dbs_IsBlob(&oldData)) { |
|
419 dbs_removeBlob(dbsp, &oldData); |
|
420 } |
|
421 |
|
422 if (data->size > DBS_MAX_ENTRY_SIZE) { |
|
423 dbs_mkBlob(dbsp,key,data,&blob); |
|
424 ret = dbs_writeBlob(dbsp, dbsp->mode, &blob, data); |
|
425 data = &blob; |
|
426 } |
|
427 } |
|
428 |
|
429 if (ret == 0) { |
|
430 ret = (* db->put)(db, key, data, flags); |
|
431 } |
|
432 return(ret); |
|
433 } |
|
434 |
|
435 static int |
|
436 dbs_sync(const DB *dbs, unsigned int flags) |
|
437 { |
|
438 DB *db = (DB *)dbs->internal; |
|
439 DBS *dbsp = (DBS *)dbs; |
|
440 |
|
441 dbs_freemap(dbsp); |
|
442 |
|
443 return (* db->sync)(db, flags); |
|
444 } |
|
445 |
|
446 static int |
|
447 dbs_del(const DB *dbs, const DBT *key, unsigned int flags) |
|
448 { |
|
449 int ret; |
|
450 DBS *dbsp = (DBS *)dbs; |
|
451 DB *db = (DB *)dbs->internal; |
|
452 |
|
453 dbs_freemap(dbsp); |
|
454 |
|
455 if (!dbsp->readOnly) { |
|
456 DBT oldData; |
|
457 ret = (*db->get)(db,key,&oldData,0); |
|
458 if ((ret == 0) && dbs_IsBlob(&oldData)) { |
|
459 dbs_removeBlob(dbsp,&oldData); |
|
460 } |
|
461 } |
|
462 |
|
463 return (* db->del)(db, key, flags); |
|
464 } |
|
465 |
|
466 static int |
|
467 dbs_seq(const DB *dbs, DBT *key, DBT *data, unsigned int flags) |
|
468 { |
|
469 int ret; |
|
470 DBS *dbsp = (DBS *)dbs; |
|
471 DB *db = (DB *)dbs->internal; |
|
472 |
|
473 dbs_freemap(dbsp); |
|
474 |
|
475 ret = (* db->seq)(db, key, data, flags); |
|
476 if ((ret == 0) && dbs_IsBlob(data)) { |
|
477 /* don't return a blob read as an error so traversals keep going */ |
|
478 (void) dbs_readBlob(dbsp,data); |
|
479 } |
|
480 |
|
481 return(ret); |
|
482 } |
|
483 |
|
484 static int |
|
485 dbs_close(DB *dbs) |
|
486 { |
|
487 DBS *dbsp = (DBS *)dbs; |
|
488 DB *db = (DB *)dbs->internal; |
|
489 int ret; |
|
490 |
|
491 dbs_freemap(dbsp); |
|
492 ret = (* db->close)(db); |
|
493 PORT_Free(dbsp->blobdir); |
|
494 PORT_Free(dbsp); |
|
495 return ret; |
|
496 } |
|
497 |
|
498 static int |
|
499 dbs_fd(const DB *dbs) |
|
500 { |
|
501 DB *db = (DB *)dbs->internal; |
|
502 |
|
503 return (* db->fd)(db); |
|
504 } |
|
505 |
|
506 /* |
|
507 * the naming convention we use is |
|
508 * change the .xxx into .dir. (for nss it's always .db); |
|
509 * if no .extension exists or is equal to .dir, add a .dir |
|
510 * the returned data must be freed. |
|
511 */ |
|
512 #define DIRSUFFIX ".dir" |
|
513 static char * |
|
514 dbs_mkBlobDirName(const char *dbname) |
|
515 { |
|
516 int dbname_len = PORT_Strlen(dbname); |
|
517 int dbname_end = dbname_len; |
|
518 const char *cp; |
|
519 char *blobDir = NULL; |
|
520 |
|
521 /* scan back from the end looking for either a directory separator, a '.', |
|
522 * or the end of the string. NOTE: Windows should check for both separators |
|
523 * here. For now this is safe because we know NSS always uses a '.' |
|
524 */ |
|
525 for (cp = &dbname[dbname_len]; |
|
526 (cp > dbname) && (*cp != '.') && (*cp != *PATH_SEPARATOR) ; |
|
527 cp--) |
|
528 /* Empty */ ; |
|
529 if (*cp == '.') { |
|
530 dbname_end = cp - dbname; |
|
531 if (PORT_Strcmp(cp,DIRSUFFIX) == 0) { |
|
532 dbname_end = dbname_len; |
|
533 } |
|
534 } |
|
535 blobDir = PORT_ZAlloc(dbname_end+sizeof(DIRSUFFIX)); |
|
536 if (blobDir == NULL) { |
|
537 return NULL; |
|
538 } |
|
539 PORT_Memcpy(blobDir,dbname,dbname_end); |
|
540 PORT_Memcpy(&blobDir[dbname_end],DIRSUFFIX,sizeof(DIRSUFFIX)); |
|
541 return blobDir; |
|
542 } |
|
543 |
|
544 #define DBM_DEFAULT 0 |
|
545 static const HASHINFO dbs_hashInfo = { |
|
546 DBS_BLOCK_SIZE, /* bucket size, must be greater than = to |
|
547 * or maximum entry size (+ header) |
|
548 * we allow before blobing */ |
|
549 DBM_DEFAULT, /* Fill Factor */ |
|
550 DBM_DEFAULT, /* number of elements */ |
|
551 DBS_CACHE_SIZE, /* cache size */ |
|
552 DBM_DEFAULT, /* hash function */ |
|
553 DBM_DEFAULT, /* byte order */ |
|
554 }; |
|
555 |
|
556 /* |
|
557 * the open function. NOTE: this is the only exposed function in this file. |
|
558 * everything else is called through the function table pointer. |
|
559 */ |
|
560 DB * |
|
561 dbsopen(const char *dbname, int flags, int mode, DBTYPE type, |
|
562 const void *userData) |
|
563 { |
|
564 DB *db = NULL,*dbs = NULL; |
|
565 DBS *dbsp = NULL; |
|
566 |
|
567 /* NOTE: we are overriding userData with dbs_hashInfo. since all known |
|
568 * callers pass 0, this is ok, otherwise we should merge the two */ |
|
569 |
|
570 dbsp = (DBS *)PORT_ZAlloc(sizeof(DBS)); |
|
571 if (!dbsp) { |
|
572 return NULL; |
|
573 } |
|
574 dbs = &dbsp->db; |
|
575 |
|
576 dbsp->blobdir=dbs_mkBlobDirName(dbname); |
|
577 if (dbsp->blobdir == NULL) { |
|
578 goto loser; |
|
579 } |
|
580 dbsp->mode = mode; |
|
581 dbsp->readOnly = (PRBool)(flags == NO_RDONLY); |
|
582 dbsp->dbs_mapfile = NULL; |
|
583 dbsp->dbs_addr = NULL; |
|
584 dbsp->dbs_len = 0; |
|
585 |
|
586 /* the real dbm call */ |
|
587 db = dbopen(dbname, flags, mode, type, &dbs_hashInfo); |
|
588 if (db == NULL) { |
|
589 goto loser; |
|
590 } |
|
591 dbs->internal = (void *) db; |
|
592 dbs->type = type; |
|
593 dbs->close = dbs_close; |
|
594 dbs->get = dbs_get; |
|
595 dbs->del = dbs_del; |
|
596 dbs->put = dbs_put; |
|
597 dbs->seq = dbs_seq; |
|
598 dbs->sync = dbs_sync; |
|
599 dbs->fd = dbs_fd; |
|
600 |
|
601 return dbs; |
|
602 loser: |
|
603 if (db) { |
|
604 (*db->close)(db); |
|
605 } |
|
606 if (dbsp) { |
|
607 if (dbsp->blobdir) { |
|
608 PORT_Free(dbsp->blobdir); |
|
609 } |
|
610 PORT_Free(dbsp); |
|
611 } |
|
612 return NULL; |
|
613 } |