pax_global_header00006660000000000000000000000064132134234540014513gustar00rootroot0000000000000052 comment=80fa6dafd52e5930d3119313d3e0d883867af2f1 sdbm-1.0.0/000077500000000000000000000000001321342345400124365ustar00rootroot00000000000000sdbm-1.0.0/.gitignore000066400000000000000000000001431321342345400144240ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ sdbm.bundle sdbm-1.0.0/.travis.yml000066400000000000000000000001461321342345400145500ustar00rootroot00000000000000sudo: false language: ruby rvm: - 2.3.5 - 2.4.2 - ruby-head before_install: gem install bundler sdbm-1.0.0/Gemfile000066400000000000000000000000471321342345400137320ustar00rootroot00000000000000source 'https://rubygems.org' gemspec sdbm-1.0.0/LICENSE.txt000066400000000000000000000024021321342345400142570ustar00rootroot00000000000000Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sdbm-1.0.0/README.md000066400000000000000000000036341321342345400137230ustar00rootroot00000000000000# SDBM [![Build Status](https://travis-ci.org/ruby/sdbm.svg?branch=enable-travis)](https://travis-ci.org/ruby/sdbm) SDBM provides a simple file-based key-value store, which can only store String keys and values. Note that Ruby comes with the source code for SDBM, while the DBM and GDBM standard libraries rely on external libraries and headers. ## Installation Add this line to your application's Gemfile: ```ruby gem 'sdbm' ``` And then execute: $ bundle Or install it yourself as: $ gem install sdbm ## Usage ### Insert values: ``` require 'sdbm' SDBM.open 'my_database' do |db| db['apple'] = 'fruit' db['pear'] = 'fruit' db['carrot'] = 'vegetable' db['tomato'] = 'vegetable' end ``` ### Bulk update: ``` require 'sdbm' SDBM.open 'my_database' do |db| db.update('peach' => 'fruit', 'tomato' => 'fruit') end ``` ### Retrieve values: ``` require 'sdbm' SDBM.open 'my_database' do |db| db.each do |key, value| puts "Key: #{key}, Value: #{value}" end end ``` ### Outputs: ``` Key: apple, Value: fruit Key: pear, Value: fruit Key: carrot, Value: vegetable Key: peach, Value: fruit Key: tomato, Value: fruit ``` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/sdbm. ## License The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). sdbm-1.0.0/Rakefile000066400000000000000000000004201321342345400140770ustar00rootroot00000000000000require "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList['test/**/test_*.rb'] end require 'rake/extensiontask' Rake::ExtensionTask.new("sdbm") task :default => [:compile, :test] sdbm-1.0.0/bin/000077500000000000000000000000001321342345400132065ustar00rootroot00000000000000sdbm-1.0.0/bin/console000077500000000000000000000005231321342345400145760ustar00rootroot00000000000000#!/usr/bin/env ruby require "bundler/setup" require "sdbm" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) sdbm-1.0.0/bin/setup000077500000000000000000000002031321342345400142670ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here sdbm-1.0.0/ext/000077500000000000000000000000001321342345400132365ustar00rootroot00000000000000sdbm-1.0.0/ext/sdbm/000077500000000000000000000000001321342345400141635ustar00rootroot00000000000000sdbm-1.0.0/ext/sdbm/_sdbm.c000066400000000000000000000504261321342345400154220ustar00rootroot00000000000000/* * sdbm - ndbm work-alike hashed database library * based on Per-Aake Larson's Dynamic Hashing algorithms. BIT 18 (1978). * author: oz@nexus.yorku.ca * status: public domain. * * core routines */ #include "ruby/ruby.h" #ifdef HAVE_UNISTD_H #include #endif #include "sdbm.h" /* * sdbm - ndbm work-alike hashed database library * tuning and portability constructs [not nearly enough] * author: oz@nexus.yorku.ca */ #define BYTESIZ 8 #ifdef BSD42 #define SEEK_SET L_SET #define memset(s,c,n) bzero((s), (n)) /* only when c is zero */ #define memcpy(s1,s2,n) bcopy((s2), (s1), (n)) #define memcmp(s1,s2,n) bcmp((s1),(s2),(n)) #endif /* * important tuning parms (hah) */ #ifndef SEEDUPS #define SEEDUPS 1 /* always detect duplicates */ #endif #ifndef BADMESS #define BADMESS 1 /* generate a message for worst case: cannot make room after SPLTMAX splits */ #endif /* * misc */ #ifdef DEBUG #define debug(x) printf x #else #define debug(x) #endif #ifdef BIG_E #define GET_SHORT(p, i) (((unsigned)((unsigned char *)(p))[(i)*2] << 8) + (((unsigned char *)(p))[(i)*2 + 1])) #define PUT_SHORT(p, i, s) (((unsigned char *)(p))[(i)*2] = (unsigned char)((s) >> 8), ((unsigned char *)(p))[(i)*2 + 1] = (unsigned char)(s)) #else #define GET_SHORT(p, i) ((p)[(i)]) #define PUT_SHORT(p, i, s) ((p)[(i)] = (s)) #endif /*#include "pair.h"*/ static int fitpair proto((char *, int)); static void putpair proto((char *, datum, datum)); static datum getpair proto((char *, datum)); static int delpair proto((char *, datum)); static int chkpage proto((char *)); static datum getnkey proto((char *, int)); static void splpage proto((char *, char *, long)); #if SEEDUPS static int duppair proto((char *, datum)); #endif #include #include #ifdef DOSISH #include #endif #include #include #ifdef BSD42 #include #else #include /*#include */ #endif #ifndef O_BINARY #define O_BINARY 0 #endif #include #ifndef EPERM #define EPERM EACCES #endif #include #ifdef __STDC__ #include #endif #ifndef NULL #define NULL 0 #endif /* * externals */ #if !defined(__sun) && !defined(_WIN32) && !defined(__CYGWIN__) && !defined(errno) extern int errno; #endif /* * forward */ static int getdbit proto((DBM *, long)); static int setdbit proto((DBM *, long)); static int getpage proto((DBM *, long)); static datum getnext proto((DBM *)); static int makroom proto((DBM *, long, int)); /* * useful macros */ #define bad(x) ((x).dptr == NULL || (x).dsize < 0) #define exhash(item) sdbm_hash((item).dptr, (item).dsize) #define ioerr(db) ((db)->flags |= DBM_IOERR) #define OFF_PAG(off) (long) (off) * PBLKSIZ #define OFF_DIR(off) (long) (off) * DBLKSIZ static long masks[] = { 000000000000L, 000000000001L, 000000000003L, 000000000007L, 000000000017L, 000000000037L, 000000000077L, 000000000177L, 000000000377L, 000000000777L, 000000001777L, 000000003777L, 000000007777L, 000000017777L, 000000037777L, 000000077777L, 000000177777L, 000000377777L, 000000777777L, 000001777777L, 000003777777L, 000007777777L, 000017777777L, 000037777777L, 000077777777L, 000177777777L, 000377777777L, 000777777777L, 001777777777L, 003777777777L, 007777777777L, 017777777777L }; datum nullitem = {NULL, 0}; DBM * sdbm_open(register char *file, register int flags, register int mode) { register DBM *db; register char *dirname; register char *pagname; register size_t n; if (file == NULL || !*file) return errno = EINVAL, (DBM *) NULL; /* * need space for two separate filenames */ n = strlen(file) * 2 + strlen(DIRFEXT) + strlen(PAGFEXT) + 2; if ((dirname = malloc(n)) == NULL) return errno = ENOMEM, (DBM *) NULL; /* * build the file names */ dirname = strcat(strcpy(dirname, file), DIRFEXT); pagname = strcpy(dirname + strlen(dirname) + 1, file); pagname = strcat(pagname, PAGFEXT); db = sdbm_prep(dirname, pagname, flags, mode); free((char *) dirname); return db; } static int fd_set_cloexec(int fd) { /* MinGW don't have F_GETFD and FD_CLOEXEC. [ruby-core:40281] */ #ifdef F_GETFD int flags, ret; flags = fcntl(fd, F_GETFD); /* should not fail except EBADF. */ if (flags == -1) { return -1; } if (2 < fd) { if (!(flags & FD_CLOEXEC)) { flags |= FD_CLOEXEC; ret = fcntl(fd, F_SETFD, flags); if (ret == -1) { return -1; } } } #endif return 0; } DBM * sdbm_prep(char *dirname, char *pagname, int flags, int mode) { register DBM *db; struct stat dstat; if ((db = (DBM *) malloc(sizeof(DBM))) == NULL) return errno = ENOMEM, (DBM *) NULL; db->pagf = -1; db->dirf = -1; db->flags = 0; db->hmask = 0; db->blkptr = 0; db->keyptr = 0; /* * adjust user flags so that WRONLY becomes RDWR, * as required by this package. Also set our internal * flag for RDONLY. */ if (flags & O_WRONLY) flags = (flags & ~O_WRONLY) | O_RDWR; if (flags & O_RDONLY) db->flags = DBM_RDONLY; /* * open the files in sequence, and stat the dirfile. * If we fail anywhere, undo everything, return NULL. */ flags |= O_BINARY; #ifdef O_CLOEXEC flags |= O_CLOEXEC; #endif if ((db->pagf = open(pagname, flags, mode)) == -1) goto err; if (fd_set_cloexec(db->pagf) == -1) goto err; if ((db->dirf = open(dirname, flags, mode)) == -1) goto err; if (fd_set_cloexec(db->dirf) == -1) goto err; /* * need the dirfile size to establish max bit number. */ if (fstat(db->dirf, &dstat) == -1) goto err; /* * zero size: either a fresh database, or one with a single, * unsplit data page: dirpage is all zeros. */ db->dirbno = (!dstat.st_size) ? 0 : -1; db->pagbno = -1; db->maxbno = dstat.st_size * (long) BYTESIZ; (void) memset(db->pagbuf, 0, PBLKSIZ); (void) memset(db->dirbuf, 0, DBLKSIZ); /* * success */ return db; err: if (db->pagf != -1) (void) close(db->pagf); if (db->dirf != -1) (void) close(db->dirf); free((char *) db); return (DBM *) NULL; } void sdbm_close(register DBM *db) { if (db == NULL) errno = EINVAL; else { (void) close(db->dirf); (void) close(db->pagf); free((char *) db); } } datum sdbm_fetch(register DBM *db, datum key) { if (db == NULL || bad(key)) return errno = EINVAL, nullitem; if (getpage(db, exhash(key))) return getpair(db->pagbuf, key); return ioerr(db), nullitem; } int sdbm_delete(register DBM *db, datum key) { if (db == NULL || bad(key)) return errno = EINVAL, -1; if (sdbm_rdonly(db)) return errno = EPERM, -1; if (getpage(db, exhash(key))) { if (!delpair(db->pagbuf, key)) return -1; /* * update the page file */ if (lseek(db->pagf, OFF_PAG(db->pagbno), SEEK_SET) < 0 || write(db->pagf, db->pagbuf, PBLKSIZ) < 0) return ioerr(db), -1; return 0; } return ioerr(db), -1; } int sdbm_store(register DBM *db, datum key, datum val, int flags) { int need; register long hash; if (db == NULL || bad(key)) return errno = EINVAL, -1; if (sdbm_rdonly(db)) return errno = EPERM, -1; need = key.dsize + val.dsize; /* * is the pair too big (or too small) for this database ?? */ if (need < 0 || need > PAIRMAX) return errno = EINVAL, -1; if (getpage(db, (hash = exhash(key)))) { /* * if we need to replace, delete the key/data pair * first. If it is not there, ignore. */ if (flags == DBM_REPLACE) (void) delpair(db->pagbuf, key); #if SEEDUPS else if (duppair(db->pagbuf, key)) return 1; #endif /* * if we do not have enough room, we have to split. */ if (!fitpair(db->pagbuf, need)) if (!makroom(db, hash, need)) return ioerr(db), -1; /* * we have enough room or split is successful. insert the key, * and update the page file. */ (void) putpair(db->pagbuf, key, val); if (lseek(db->pagf, OFF_PAG(db->pagbno), SEEK_SET) < 0 || write(db->pagf, db->pagbuf, PBLKSIZ) < 0) return ioerr(db), -1; /* * success */ return 0; } return ioerr(db), -1; } /* * makroom - make room by splitting the overfull page * this routine will attempt to make room for SPLTMAX times before * giving up. */ static int makroom(register DBM *db, long int hash, int need) { long newp; char twin[PBLKSIZ]; #if defined _WIN32 char zer[PBLKSIZ]; long oldtail; #endif char *pag = db->pagbuf; char *new = twin; register int smax = SPLTMAX; do { /* * split the current page */ (void) splpage(pag, new, db->hmask + 1); /* * address of the new page */ newp = (hash & db->hmask) | (db->hmask + 1); debug(("newp: %ld\n", newp)); /* * write delay, read avoidance/cache shuffle: * select the page for incoming pair: if key is to go to the new page, * write out the previous one, and copy the new one over, thus making * it the current page. If not, simply write the new page, and we are * still looking at the page of interest. current page is not updated * here, as sdbm_store will do so, after it inserts the incoming pair. */ #if defined _WIN32 /* * Fill hole with 0 if made it. * (hole is NOT read as 0) */ oldtail = lseek(db->pagf, 0L, SEEK_END); memset(zer, 0, PBLKSIZ); while (OFF_PAG(newp) > oldtail) { if (lseek(db->pagf, 0L, SEEK_END) < 0 || write(db->pagf, zer, PBLKSIZ) < 0) { return 0; } oldtail += PBLKSIZ; } #endif if (hash & (db->hmask + 1)) { if (lseek(db->pagf, OFF_PAG(db->pagbno), SEEK_SET) < 0 || write(db->pagf, db->pagbuf, PBLKSIZ) < 0) return 0; db->pagbno = newp; (void) memcpy(pag, new, PBLKSIZ); } else if (lseek(db->pagf, OFF_PAG(newp), SEEK_SET) < 0 || write(db->pagf, new, PBLKSIZ) < 0) return 0; if (!setdbit(db, db->curbit)) return 0; /* * see if we have enough room now */ if (fitpair(pag, need)) return 1; /* * try again... update curbit and hmask as getpage would have * done. because of our update of the current page, we do not * need to read in anything. BUT we have to write the current * [deferred] page out, as the window of failure is too great. */ db->curbit = 2 * db->curbit + ((hash & (db->hmask + 1)) ? 2 : 1); db->hmask |= (db->hmask + 1); if (lseek(db->pagf, OFF_PAG(db->pagbno), SEEK_SET) < 0 || write(db->pagf, db->pagbuf, PBLKSIZ) < 0) return 0; } while (--smax); /* * if we are here, this is real bad news. After SPLTMAX splits, * we still cannot fit the key. say goodnight. */ #if BADMESS (void) (write(2, "sdbm: cannot insert after SPLTMAX attempts.\n", 44) < 0); #endif return 0; } /* * the following two routines will break if * deletions aren't taken into account. (ndbm bug) */ datum sdbm_firstkey(register DBM *db) { if (db == NULL) return errno = EINVAL, nullitem; /* * start at page 0 */ (void) memset(db->pagbuf, 0, PBLKSIZ); if (lseek(db->pagf, OFF_PAG(0), SEEK_SET) < 0 || read(db->pagf, db->pagbuf, PBLKSIZ) < 0) return ioerr(db), nullitem; db->pagbno = 0; db->blkptr = 0; db->keyptr = 0; return getnext(db); } datum sdbm_nextkey(register DBM *db) { if (db == NULL) return errno = EINVAL, nullitem; return getnext(db); } /* * all important binary trie traversal */ static int getpage(register DBM *db, register long int hash) { register int hbit; register long dbit; register long pagb; dbit = 0; hbit = 0; while (dbit < db->maxbno && getdbit(db, dbit)) dbit = 2 * dbit + ((hash & ((long) 1 << hbit++)) ? 2 : 1); debug(("dbit: %ld...", dbit)); db->curbit = dbit; db->hmask = masks[hbit]; pagb = hash & db->hmask; /* * see if the block we need is already in memory. * note: this lookaside cache has about 10% hit rate. */ if (pagb != db->pagbno) { /* * note: here, we assume a "hole" is read as 0s. * if not, must zero pagbuf first. */ (void) memset(db->pagbuf, 0, PBLKSIZ); if (lseek(db->pagf, OFF_PAG(pagb), SEEK_SET) < 0 || read(db->pagf, db->pagbuf, PBLKSIZ) < 0) return 0; if (!chkpage(db->pagbuf)) { return 0; } db->pagbno = pagb; debug(("pag read: %ld\n", pagb)); } return 1; } static int getdbit(register DBM *db, register long int dbit) { register long c; register long dirb; c = dbit / BYTESIZ; dirb = c / DBLKSIZ; if (dirb != db->dirbno) { if (lseek(db->dirf, OFF_DIR(dirb), SEEK_SET) < 0 || read(db->dirf, db->dirbuf, DBLKSIZ) < 0) return 0; db->dirbno = dirb; debug(("dir read: %ld\n", dirb)); } return db->dirbuf[c % DBLKSIZ] & (1 << (dbit % BYTESIZ)); } static int setdbit(register DBM *db, register long int dbit) { register long c; register long dirb; c = dbit / BYTESIZ; dirb = c / DBLKSIZ; if (dirb != db->dirbno) { if (lseek(db->dirf, OFF_DIR(dirb), SEEK_SET) < 0 || read(db->dirf, db->dirbuf, DBLKSIZ) < 0) return 0; db->dirbno = dirb; debug(("dir read: %ld\n", dirb)); } db->dirbuf[c % DBLKSIZ] |= (1 << (dbit % BYTESIZ)); if (dbit >= db->maxbno) db->maxbno += (long) DBLKSIZ * BYTESIZ; if (lseek(db->dirf, OFF_DIR(dirb), SEEK_SET) < 0 || write(db->dirf, db->dirbuf, DBLKSIZ) < 0) return 0; return 1; } /* * getnext - get the next key in the page, and if done with * the page, try the next page in sequence */ static datum getnext(register DBM *db) { datum key; for (;;) { db->keyptr++; key = getnkey(db->pagbuf, db->keyptr); if (key.dptr != NULL) return key; /* * we either run out, or there is nothing on this page.. * try the next one... If we lost our position on the * file, we will have to seek. */ db->keyptr = 0; if (db->pagbno != db->blkptr++) if (lseek(db->pagf, OFF_PAG(db->blkptr), SEEK_SET) < 0) break; db->pagbno = db->blkptr; if (read(db->pagf, db->pagbuf, PBLKSIZ) <= 0) break; if (!chkpage(db->pagbuf)) { break; } } return ioerr(db), nullitem; } /* pair.c */ /* * sdbm - ndbm work-alike hashed database library * based on Per-Aake Larson's Dynamic Hashing algorithms. BIT 18 (1978). * author: oz@nexus.yorku.ca * status: public domain. * * page-level routines */ #ifndef BSD42 /*#include */ #endif #define exhash(item) sdbm_hash((item).dptr, (item).dsize) /* * forward */ static int seepair proto((char *, int, char *, int)); /* * page format: * +------------------------------+ * ino | n | keyoff | datoff | keyoff | * +------------+--------+--------+ * | datoff | - - - ----> | * +--------+---------------------+ * | F R E E A R E A | * +--------------+---------------+ * | <---- - - - | data | * +--------+-----+----+----------+ * | key | data | key | * +--------+----------+----------+ * * calculating the offsets for free area: if the number * of entries (ino[0]) is zero, the offset to the END of * the free area is the block size. Otherwise, it is the * nth (ino[ino[0]]) entry's offset. */ static int fitpair(char *pag, int need) { register int n; register int off; register int free; register short *ino = (short *) pag; off = ((n = GET_SHORT(ino,0)) > 0) ? GET_SHORT(ino,n) : PBLKSIZ; free = off - (n + 1) * (int)sizeof(short); need += 2 * (int)sizeof(short); debug(("free %d need %d\n", free, need)); return need <= free; } static void putpair(char *pag, datum key, datum val) { register int n; register int off; register short *ino = (short *) pag; off = ((n = GET_SHORT(ino,0)) > 0) ? GET_SHORT(ino,n) : PBLKSIZ; /* * enter the key first */ off -= key.dsize; if (key.dsize) (void) memcpy(pag + off, key.dptr, key.dsize); PUT_SHORT(ino,n + 1,off); /* * now the data */ off -= val.dsize; if (val.dsize) (void) memcpy(pag + off, val.dptr, val.dsize); PUT_SHORT(ino,n + 2,off); /* * adjust item count */ PUT_SHORT(ino,0,GET_SHORT(ino,0) + 2); } static datum getpair(char *pag, datum key) { register int i; register int n; datum val; register short *ino = (short *) pag; if ((n = GET_SHORT(ino,0)) == 0) return nullitem; if ((i = seepair(pag, n, key.dptr, key.dsize)) == 0) return nullitem; val.dptr = pag + GET_SHORT(ino,i + 1); val.dsize = GET_SHORT(ino,i) - GET_SHORT(ino,i + 1); return val; } #if SEEDUPS static int duppair(char *pag, datum key) { register short *ino = (short *) pag; return GET_SHORT(ino,0) > 0 && seepair(pag, GET_SHORT(ino,0), key.dptr, key.dsize) > 0; } #endif static datum getnkey(char *pag, int num) { datum key; register int off; register short *ino = (short *) pag; num = num * 2 - 1; if (GET_SHORT(ino,0) == 0 || num > GET_SHORT(ino,0)) return nullitem; off = (num > 1) ? GET_SHORT(ino,num - 1) : PBLKSIZ; key.dptr = pag + GET_SHORT(ino,num); key.dsize = off - GET_SHORT(ino,num); return key; } static int delpair(char *pag, datum key) { register int n; register int i; register short *ino = (short *) pag; if ((n = GET_SHORT(ino,0)) == 0) return 0; if ((i = seepair(pag, n, key.dptr, key.dsize)) == 0) return 0; /* * found the key. if it is the last entry * [i.e. i == n - 1] we just adjust the entry count. * hard case: move all data down onto the deleted pair, * shift offsets onto deleted offsets, and adjust them. * [note: 0 < i < n] */ if (i < n - 1) { register int m; register char *dst = pag + (i == 1 ? PBLKSIZ : GET_SHORT(ino,i - 1)); register char *src = pag + GET_SHORT(ino,i + 1); register ptrdiff_t zoo = dst - src; debug(("free-up %"PRIdPTRDIFF" ", zoo)); /* * shift data/keys down */ m = GET_SHORT(ino,i + 1) - GET_SHORT(ino,n); #ifdef DUFF #define MOVB *--dst = *--src if (m > 0) { register int loop = (m + 8 - 1) >> 3; switch (m & (8 - 1)) { case 0: do { MOVB; case 7: MOVB; case 6: MOVB; case 5: MOVB; case 4: MOVB; case 3: MOVB; case 2: MOVB; case 1: MOVB; } while (--loop); } } #else #ifdef MEMMOVE memmove(dst-m, src-m, m); #else while (m--) *--dst = *--src; #endif #endif /* * adjust offset index up */ while (i < n - 1) { PUT_SHORT(ino,i, GET_SHORT(ino,i + 2) + zoo); i++; } } PUT_SHORT(ino, 0, GET_SHORT(ino, 0) - 2); return 1; } /* * search for the key in the page. * return offset index in the range 0 < i < n. * return 0 if not found. */ static int seepair(char *pag, register int n, register char *key, register int siz) { register int i; register int off = PBLKSIZ; register short *ino = (short *) pag; for (i = 1; i < n; i += 2) { if (siz == off - GET_SHORT(ino,i) && memcmp(key, pag + GET_SHORT(ino,i), siz) == 0) return i; off = GET_SHORT(ino,i + 1); } return 0; } static void splpage(char *pag, char *new, long int sbit) { datum key; datum val; register int n; register int off = PBLKSIZ; char cur[PBLKSIZ]; register short *ino = (short *) cur; (void) memcpy(cur, pag, PBLKSIZ); (void) memset(pag, 0, PBLKSIZ); (void) memset(new, 0, PBLKSIZ); n = GET_SHORT(ino,0); for (ino++; n > 0; ino += 2) { key.dptr = cur + GET_SHORT(ino,0); key.dsize = off - GET_SHORT(ino,0); val.dptr = cur + GET_SHORT(ino,1); val.dsize = GET_SHORT(ino,0) - GET_SHORT(ino,1); /* * select the page pointer (by looking at sbit) and insert */ (void) putpair((exhash(key) & sbit) ? new : pag, key, val); off = GET_SHORT(ino,1); n -= 2; } debug(("%d split %d/%d\n", ((short *) cur)[0] / 2, ((short *) new)[0] / 2, ((short *) pag)[0] / 2)); } /* * check page sanity: * number of entries should be something * reasonable, and all offsets in the index should be in order. * this could be made more rigorous. */ static int chkpage(char *pag) { register int n; register int off; register short *ino = (short *) pag; if ((n = GET_SHORT(ino,0)) < 0 || n > PBLKSIZ / (int)sizeof(short)) return 0; if (n > 0) { off = PBLKSIZ; for (ino++; n > 0; ino += 2) { if (GET_SHORT(ino,0) > off || GET_SHORT(ino,1) > off || GET_SHORT(ino,1) > GET_SHORT(ino,0)) return 0; off = GET_SHORT(ino,1); n -= 2; } } return 1; } /* hash.c */ /* * sdbm - ndbm work-alike hashed database library * based on Per-Aake Larson's Dynamic Hashing algorithms. BIT 18 (1978). * author: oz@nexus.yorku.ca * status: public domain. keep it that way. * * hashing routine */ /* * polynomial conversion ignoring overflows * [this seems to work remarkably well, in fact better * then the ndbm hash function. Replace at your own risk] * use: 65599 nice. * 65587 even better. */ long sdbm_hash(register char *str, register int len) { register unsigned long n = 0; #ifdef DUFF #define HASHC n = *str++ + 65599 * n if (len > 0) { register int loop = (len + 8 - 1) >> 3; switch(len & (8 - 1)) { case 0: do { HASHC; case 7: HASHC; case 6: HASHC; case 5: HASHC; case 4: HASHC; case 3: HASHC; case 2: HASHC; case 1: HASHC; } while (--loop); } } #else while (len--) n = ((*str++) & 255) + 65587L * n; #endif return n; } sdbm-1.0.0/ext/sdbm/extconf.rb000066400000000000000000000001401321342345400161510ustar00rootroot00000000000000# frozen_string_literal: false require 'mkmf' $defs << "-D""BADMESS=0" create_makefile("sdbm") sdbm-1.0.0/ext/sdbm/init.c000066400000000000000000000600121321342345400152710ustar00rootroot00000000000000/************************************************ sdbminit.c - $Author$ created at: Fri May 7 08:34:24 JST 1999 Copyright (C) 1995-2001 Yukihiro Matsumoto ************************************************/ #include "ruby.h" #include "sdbm.h" #include #include /* * Document-class: SDBM * * SDBM provides a simple file-based key-value store, which can only store * String keys and values. * * Note that Ruby comes with the source code for SDBM, while the DBM and GDBM * standard libraries rely on external libraries and headers. * * === Examples * * Insert values: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db['apple'] = 'fruit' * db['pear'] = 'fruit' * db['carrot'] = 'vegetable' * db['tomato'] = 'vegetable' * end * * Bulk update: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db.update('peach' => 'fruit', 'tomato' => 'fruit') * end * * Retrieve values: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db.each do |key, value| * puts "Key: #{key}, Value: #{value}" * end * end * * Outputs: * * Key: apple, Value: fruit * Key: pear, Value: fruit * Key: carrot, Value: vegetable * Key: peach, Value: fruit * Key: tomato, Value: fruit */ static VALUE rb_cDBM, rb_eDBMError; struct dbmdata { int di_size; DBM *di_dbm; }; static void closed_sdbm(void) { rb_raise(rb_eDBMError, "closed SDBM file"); } #define GetDBM(obj, dbmp) do {\ TypedData_Get_Struct((obj), struct dbmdata, &sdbm_type, (dbmp));\ if ((dbmp)->di_dbm == 0) closed_sdbm();\ } while (0) #define GetDBM2(obj, dbmp, dbm) do {\ GetDBM((obj), (dbmp));\ (dbm) = (dbmp)->di_dbm;\ } while (0) static void free_sdbm(void *ptr) { struct dbmdata *dbmp = ptr; if (dbmp->di_dbm) sdbm_close(dbmp->di_dbm); ruby_xfree(dbmp); } static size_t memsize_dbm(const void *ptr) { const struct dbmdata *dbmp = ptr; size_t size = sizeof(*dbmp); if (dbmp->di_dbm) size += sizeof(DBM); return size; } static const rb_data_type_t sdbm_type = { "sdbm", {0, free_sdbm, memsize_dbm,}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, }; /* * call-seq: * sdbm.close -> nil * * Closes the database file. * * Raises SDBMError if the database is already closed. */ static VALUE fsdbm_close(VALUE obj) { struct dbmdata *dbmp; GetDBM(obj, dbmp); sdbm_close(dbmp->di_dbm); dbmp->di_dbm = 0; return Qnil; } /* * call-seq: * sdbm.closed? -> true or false * * Returns +true+ if the database is closed. */ static VALUE fsdbm_closed(VALUE obj) { struct dbmdata *dbmp; TypedData_Get_Struct(obj, struct dbmdata, &sdbm_type, dbmp); if (dbmp->di_dbm == 0) return Qtrue; return Qfalse; } static VALUE fsdbm_alloc(VALUE klass) { struct dbmdata *dbmp; return TypedData_Make_Struct(klass, struct dbmdata, &sdbm_type, dbmp); } /* * call-seq: * SDBM.new(filename, mode = 0666) * * Creates a new database handle by opening the given +filename+. SDBM actually * uses two physical files, with extensions '.dir' and '.pag'. These extensions * will automatically be appended to the +filename+. * * If the file does not exist, a new file will be created using the given * +mode+, unless +mode+ is explicitly set to nil. In the latter case, no * database will be created. * * If the file exists, it will be opened in read/write mode. If this fails, it * will be opened in read-only mode. */ static VALUE fsdbm_initialize(int argc, VALUE *argv, VALUE obj) { VALUE file, vmode; DBM *dbm; struct dbmdata *dbmp; int mode; TypedData_Get_Struct(obj, struct dbmdata, &sdbm_type, dbmp); if (rb_scan_args(argc, argv, "11", &file, &vmode) == 1) { mode = 0666; /* default value */ } else if (NIL_P(vmode)) { mode = -1; /* return nil if DB not exist */ } else { mode = NUM2INT(vmode); } FilePathValue(file); dbm = 0; if (mode >= 0) dbm = sdbm_open(RSTRING_PTR(file), O_RDWR|O_CREAT, mode); if (!dbm) dbm = sdbm_open(RSTRING_PTR(file), O_RDWR, 0); if (!dbm) dbm = sdbm_open(RSTRING_PTR(file), O_RDONLY, 0); if (!dbm) { if (mode == -1) return Qnil; rb_sys_fail_str(file); } if (dbmp->di_dbm) sdbm_close(dbmp->di_dbm); dbmp->di_dbm = dbm; dbmp->di_size = -1; return obj; } /* * call-seq: * SDBM.open(filename, mode = 0666) * SDBM.open(filename, mode = 0666) { |sdbm| ... } * * If called without a block, this is the same as SDBM.new. * * If a block is given, the new database will be passed to the block and * will be safely closed after the block has executed. * * Example: * * require 'sdbm' * * SDBM.open('my_database') do |db| * db['hello'] = 'world' * end */ static VALUE fsdbm_s_open(int argc, VALUE *argv, VALUE klass) { VALUE obj = fsdbm_alloc(klass); if (NIL_P(fsdbm_initialize(argc, argv, obj))) { return Qnil; } if (rb_block_given_p()) { return rb_ensure(rb_yield, obj, fsdbm_close, obj); } return obj; } static VALUE fsdbm_fetch(VALUE obj, VALUE keystr, VALUE ifnone) { datum key, value; struct dbmdata *dbmp; DBM *dbm; ExportStringValue(keystr); key.dptr = RSTRING_PTR(keystr); key.dsize = RSTRING_LENINT(keystr); GetDBM2(obj, dbmp, dbm); value = sdbm_fetch(dbm, key); if (value.dptr == 0) { if (ifnone == Qnil && rb_block_given_p()) return rb_yield(rb_external_str_new(key.dptr, key.dsize)); return ifnone; } return rb_external_str_new(value.dptr, value.dsize); } /* * call-seq: * sdbm[key] -> value or nil * * Returns the +value+ in the database associated with the given +key+ string. * * If no value is found, returns +nil+. */ static VALUE fsdbm_aref(VALUE obj, VALUE keystr) { return fsdbm_fetch(obj, keystr, Qnil); } /* * call-seq: * sdbm.fetch(key) -> value or nil * sdbm.fetch(key) { |key| ... } * * Returns the +value+ in the database associated with the given +key+ string. * * If a block is provided, the block will be called when there is no * +value+ associated with the given +key+. The +key+ will be passed in as an * argument to the block. * * If no block is provided and no value is associated with the given +key+, * then an IndexError will be raised. */ static VALUE fsdbm_fetch_m(int argc, VALUE *argv, VALUE obj) { VALUE keystr, valstr, ifnone; rb_scan_args(argc, argv, "11", &keystr, &ifnone); valstr = fsdbm_fetch(obj, keystr, ifnone); if (argc == 1 && !rb_block_given_p() && NIL_P(valstr)) rb_raise(rb_eIndexError, "key not found"); return valstr; } /* * call-seq: * sdbm.key(value) -> key * * Returns the +key+ associated with the given +value+. If more than one * +key+ corresponds to the given +value+, then the first key to be found * will be returned. If no keys are found, +nil+ will be returned. */ static VALUE fsdbm_key(VALUE obj, VALUE valstr) { datum key, val; struct dbmdata *dbmp; DBM *dbm; ExportStringValue(valstr); val.dptr = RSTRING_PTR(valstr); val.dsize = RSTRING_LENINT(valstr); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); if (val.dsize == RSTRING_LEN(valstr) && memcmp(val.dptr, RSTRING_PTR(valstr), val.dsize) == 0) return rb_external_str_new(key.dptr, key.dsize); } return Qnil; } /* * :nodoc: */ static VALUE fsdbm_index(VALUE hash, VALUE value) { rb_warn("SDBM#index is deprecated; use SDBM#key"); return fsdbm_key(hash, value); } /* call-seq: * sdbm.select { |key, value| ... } -> Array * * Returns a new Array of key-value pairs for which the block returns +true+. * * Example: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db['apple'] = 'fruit' * db['pear'] = 'fruit' * db['spinach'] = 'vegetable' * * veggies = db.select do |key, value| * value == 'vegetable' * end #=> [["apple", "fruit"], ["pear", "fruit"]] * end */ static VALUE fsdbm_select(VALUE obj) { VALUE new = rb_ary_new(); datum key, val; DBM *dbm; struct dbmdata *dbmp; GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { VALUE assoc, v; val = sdbm_fetch(dbm, key); assoc = rb_assoc_new(rb_external_str_new(key.dptr, key.dsize), rb_external_str_new(val.dptr, val.dsize)); v = rb_yield(assoc); if (RTEST(v)) { rb_ary_push(new, assoc); } GetDBM2(obj, dbmp, dbm); } return new; } /* call-seq: * sdbm.values_at(key, ...) -> Array * * Returns an Array of values corresponding to the given keys. */ static VALUE fsdbm_values_at(int argc, VALUE *argv, VALUE obj) { VALUE new = rb_ary_new2(argc); int i; for (i=0; i value or nil * sdbm.delete(key) { |key, value| ... } * * Deletes the key-value pair corresponding to the given +key+. If the * +key+ exists, the deleted value will be returned, otherwise +nil+. * * If a block is provided, the deleted +key+ and +value+ will be passed to * the block as arguments. If the +key+ does not exist in the database, the * value will be +nil+. */ static VALUE fsdbm_delete(VALUE obj, VALUE keystr) { datum key, value; struct dbmdata *dbmp; DBM *dbm; VALUE valstr; fdbm_modify(obj); ExportStringValue(keystr); key.dptr = RSTRING_PTR(keystr); key.dsize = RSTRING_LENINT(keystr); GetDBM2(obj, dbmp, dbm); dbmp->di_size = -1; value = sdbm_fetch(dbm, key); if (value.dptr == 0) { if (rb_block_given_p()) return rb_yield(keystr); return Qnil; } /* need to save value before sdbm_delete() */ valstr = rb_external_str_new(value.dptr, value.dsize); if (sdbm_delete(dbm, key)) { dbmp->di_size = -1; rb_raise(rb_eDBMError, "dbm_delete failed"); } else if (dbmp->di_size >= 0) { dbmp->di_size--; } return valstr; } /* * call-seq: * sdbm.shift -> Array or nil * * Removes a key-value pair from the database and returns them as an * Array. If the database is empty, returns +nil+. */ static VALUE fsdbm_shift(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE keystr, valstr; fdbm_modify(obj); GetDBM2(obj, dbmp, dbm); key = sdbm_firstkey(dbm); if (!key.dptr) return Qnil; val = sdbm_fetch(dbm, key); keystr = rb_external_str_new(key.dptr, key.dsize); valstr = rb_external_str_new(val.dptr, val.dsize); sdbm_delete(dbm, key); if (dbmp->di_size >= 0) { dbmp->di_size--; } return rb_assoc_new(keystr, valstr); } /* * call-seq: * sdbm.delete_if { |key, value| ... } -> self * sdbm.reject! { |key, value| ... } -> self * * Iterates over the key-value pairs in the database, deleting those for * which the block returns +true+. */ static VALUE fsdbm_delete_if(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE keystr, valstr; VALUE ret, ary = rb_ary_new(); long i; int status = 0, n; fdbm_modify(obj); GetDBM2(obj, dbmp, dbm); n = dbmp->di_size; dbmp->di_size = -1; for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); keystr = rb_external_str_new(key.dptr, key.dsize); valstr = rb_external_str_new(val.dptr, val.dsize); ret = rb_protect(rb_yield, rb_assoc_new(rb_str_dup(keystr), valstr), &status); if (status != 0) break; if (RTEST(ret)) rb_ary_push(ary, keystr); GetDBM2(obj, dbmp, dbm); } for (i = 0; i < RARRAY_LEN(ary); i++) { keystr = RARRAY_AREF(ary, i); ExportStringValue(keystr); key.dptr = RSTRING_PTR(keystr); key.dsize = RSTRING_LENINT(keystr); if (sdbm_delete(dbm, key)) { rb_raise(rb_eDBMError, "sdbm_delete failed"); } } if (status) rb_jump_tag(status); if (n > 0) dbmp->di_size = n - RARRAY_LENINT(ary); return obj; } /* * call-seq: * sdbm.clear -> self * * Deletes all data from the database. */ static VALUE fsdbm_clear(VALUE obj) { datum key; struct dbmdata *dbmp; DBM *dbm; fdbm_modify(obj); GetDBM2(obj, dbmp, dbm); dbmp->di_size = -1; while (key = sdbm_firstkey(dbm), key.dptr) { if (sdbm_delete(dbm, key)) { rb_raise(rb_eDBMError, "sdbm_delete failed"); } } dbmp->di_size = 0; return obj; } /* * call-seq: * sdbm.invert -> Hash * * Returns a Hash in which the key-value pairs have been inverted. * * Example: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db.update('apple' => 'fruit', 'spinach' => 'vegetable') * * db.invert #=> {"fruit" => "apple", "vegetable" => "spinach"} * end */ static VALUE fsdbm_invert(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE keystr, valstr; VALUE hash = rb_hash_new(); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); keystr = rb_external_str_new(key.dptr, key.dsize); valstr = rb_external_str_new(val.dptr, val.dsize); rb_hash_aset(hash, valstr, keystr); } return hash; } /* * call-seq: * sdbm[key] = value -> value * sdbm.store(key, value) -> value * * Stores a new +value+ in the database with the given +key+ as an index. * * If the +key+ already exists, this will update the +value+ associated with * the +key+. * * Returns the given +value+. */ static VALUE fsdbm_store(VALUE obj, VALUE keystr, VALUE valstr) { datum key, val; struct dbmdata *dbmp; DBM *dbm; if (valstr == Qnil) { fsdbm_delete(obj, keystr); return Qnil; } fdbm_modify(obj); ExportStringValue(keystr); ExportStringValue(valstr); key.dptr = RSTRING_PTR(keystr); key.dsize = RSTRING_LENINT(keystr); val.dptr = RSTRING_PTR(valstr); val.dsize = RSTRING_LENINT(valstr); GetDBM2(obj, dbmp, dbm); dbmp->di_size = -1; if (sdbm_store(dbm, key, val, DBM_REPLACE)) { #ifdef HAVE_DBM_CLAERERR sdbm_clearerr(dbm); #endif if (errno == EPERM) rb_sys_fail(0); rb_raise(rb_eDBMError, "sdbm_store failed"); } return valstr; } static VALUE update_i(RB_BLOCK_CALL_FUNC_ARGLIST(pair, dbm)) { const VALUE *ptr; Check_Type(pair, T_ARRAY); if (RARRAY_LEN(pair) < 2) { rb_raise(rb_eArgError, "pair must be [key, value]"); } ptr = RARRAY_CONST_PTR(pair); fsdbm_store(dbm, ptr[0], ptr[1]); return Qnil; } /* * call-seq: * sdbm.update(pairs) -> self * * Insert or update key-value pairs. * * This method will work with any object which implements an each_pair * method, such as a Hash. */ static VALUE fsdbm_update(VALUE obj, VALUE other) { rb_block_call(other, rb_intern("each_pair"), 0, 0, update_i, obj); return obj; } /* * call-seq: * sdbm.replace(pairs) -> self * * Empties the database, then inserts the given key-value pairs. * * This method will work with any object which implements an each_pair * method, such as a Hash. */ static VALUE fsdbm_replace(VALUE obj, VALUE other) { fsdbm_clear(obj); rb_block_call(other, rb_intern("each_pair"), 0, 0, update_i, obj); return obj; } /* * call-seq: * sdbm.length -> integer * sdbm.size -> integer * * Returns the number of keys in the database. */ static VALUE fsdbm_length(VALUE obj) { datum key; struct dbmdata *dbmp; DBM *dbm; int i = 0; GetDBM2(obj, dbmp, dbm); if (dbmp->di_size > 0) return INT2FIX(dbmp->di_size); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { i++; } dbmp->di_size = i; return INT2FIX(i); } /* * call-seq: * sdbm.empty? -> true or false * * Returns +true+ if the database is empty. */ static VALUE fsdbm_empty_p(VALUE obj) { datum key; struct dbmdata *dbmp; DBM *dbm; GetDBM(obj, dbmp); if (dbmp->di_size < 0) { dbm = dbmp->di_dbm; for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { return Qfalse; } } else { if (dbmp->di_size) return Qfalse; } return Qtrue; } /* * call-seq: * sdbm.each_value * sdbm.each_value { |value| ... } * * Iterates over each +value+ in the database. * * If no block is given, returns an Enumerator. */ static VALUE fsdbm_each_value(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; RETURN_ENUMERATOR(obj, 0, 0); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); rb_yield(rb_external_str_new(val.dptr, val.dsize)); GetDBM2(obj, dbmp, dbm); } return obj; } /* * call-seq: * sdbm.each_key * sdbm.each_key { |key| ... } * * Iterates over each +key+ in the database. * * If no block is given, returns an Enumerator. */ static VALUE fsdbm_each_key(VALUE obj) { datum key; struct dbmdata *dbmp; DBM *dbm; RETURN_ENUMERATOR(obj, 0, 0); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { rb_yield(rb_external_str_new(key.dptr, key.dsize)); GetDBM2(obj, dbmp, dbm); } return obj; } /* * call-seq: * sdbm.each * sdbm.each { |key, value| ... } * sdbm.each_pair * sdbm.each_pair { |key, value| ... } * * Iterates over each key-value pair in the database. * * If no block is given, returns an Enumerator. */ static VALUE fsdbm_each_pair(VALUE obj) { datum key, val; DBM *dbm; struct dbmdata *dbmp; VALUE keystr, valstr; RETURN_ENUMERATOR(obj, 0, 0); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); keystr = rb_external_str_new(key.dptr, key.dsize); valstr = rb_external_str_new(val.dptr, val.dsize); rb_yield(rb_assoc_new(keystr, valstr)); GetDBM2(obj, dbmp, dbm); } return obj; } /* * call-seq: * sdbm.keys -> Array * * Returns a new Array containing the keys in the database. */ static VALUE fsdbm_keys(VALUE obj) { datum key; struct dbmdata *dbmp; DBM *dbm; VALUE ary; GetDBM2(obj, dbmp, dbm); ary = rb_ary_new(); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { rb_ary_push(ary, rb_external_str_new(key.dptr, key.dsize)); } return ary; } /* * call-seq: * sdbm.values -> Array * * Returns a new Array containing the values in the database. */ static VALUE fsdbm_values(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE ary; GetDBM2(obj, dbmp, dbm); ary = rb_ary_new(); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); rb_ary_push(ary, rb_external_str_new(val.dptr, val.dsize)); } return ary; } /* * call-seq: * sdbm.include?(key) -> true or false * sdbm.key?(key) -> true or false * sdbm.member?(key) -> true or false * sdbm.has_key?(key) -> true or false * * Returns +true+ if the database contains the given +key+. */ static VALUE fsdbm_has_key(VALUE obj, VALUE keystr) { datum key, val; struct dbmdata *dbmp; DBM *dbm; ExportStringValue(keystr); key.dptr = RSTRING_PTR(keystr); key.dsize = RSTRING_LENINT(keystr); GetDBM2(obj, dbmp, dbm); val = sdbm_fetch(dbm, key); if (val.dptr) return Qtrue; return Qfalse; } /* * call-seq: * sdbm.value?(key) -> true or false * sdbm.has_value?(key) -> true or false * * Returns +true+ if the database contains the given +value+. */ static VALUE fsdbm_has_value(VALUE obj, VALUE valstr) { datum key, val; struct dbmdata *dbmp; DBM *dbm; ExportStringValue(valstr); val.dptr = RSTRING_PTR(valstr); val.dsize = RSTRING_LENINT(valstr); GetDBM2(obj, dbmp, dbm); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); if (val.dsize == RSTRING_LENINT(valstr) && memcmp(val.dptr, RSTRING_PTR(valstr), val.dsize) == 0) return Qtrue; } return Qfalse; } /* * call-seq: * sdbm.to_a -> Array * * Returns a new Array containing each key-value pair in the database. * * Example: * * require 'sdbm' * * SDBM.open 'my_database' do |db| * db.update('apple' => 'fruit', 'spinach' => 'vegetable') * * db.to_a #=> [["apple", "fruit"], ["spinach", "vegetable"]] * end */ static VALUE fsdbm_to_a(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE ary; GetDBM2(obj, dbmp, dbm); ary = rb_ary_new(); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); rb_ary_push(ary, rb_assoc_new(rb_external_str_new(key.dptr, key.dsize), rb_external_str_new(val.dptr, val.dsize))); } return ary; } /* * call-seq: * sdbm.to_hash -> Hash * * Returns a new Hash containing each key-value pair in the database. */ static VALUE fsdbm_to_hash(VALUE obj) { datum key, val; struct dbmdata *dbmp; DBM *dbm; VALUE hash; GetDBM2(obj, dbmp, dbm); hash = rb_hash_new(); for (key = sdbm_firstkey(dbm); key.dptr; key = sdbm_nextkey(dbm)) { val = sdbm_fetch(dbm, key); rb_hash_aset(hash, rb_external_str_new(key.dptr, key.dsize), rb_external_str_new(val.dptr, val.dsize)); } return hash; } /* * call-seq: * sdbm.reject { |key, value| ... } -> Hash * * Creates a new Hash using the key-value pairs from the database, then * calls Hash#reject with the given block, which returns a Hash with * only the key-value pairs for which the block returns +false+. */ static VALUE fsdbm_reject(VALUE obj) { return rb_hash_delete_if(fsdbm_to_hash(obj)); } void Init_sdbm(void) { rb_cDBM = rb_define_class("SDBM", rb_cObject); rb_eDBMError = rb_define_class("SDBMError", rb_eStandardError); /* Document-class: SDBMError * Exception class used to return errors from the sdbm library. */ rb_include_module(rb_cDBM, rb_mEnumerable); rb_define_alloc_func(rb_cDBM, fsdbm_alloc); rb_define_singleton_method(rb_cDBM, "open", fsdbm_s_open, -1); rb_define_method(rb_cDBM, "initialize", fsdbm_initialize, -1); rb_define_method(rb_cDBM, "close", fsdbm_close, 0); rb_define_method(rb_cDBM, "closed?", fsdbm_closed, 0); rb_define_method(rb_cDBM, "[]", fsdbm_aref, 1); rb_define_method(rb_cDBM, "fetch", fsdbm_fetch_m, -1); rb_define_method(rb_cDBM, "[]=", fsdbm_store, 2); rb_define_method(rb_cDBM, "store", fsdbm_store, 2); rb_define_method(rb_cDBM, "index", fsdbm_index, 1); rb_define_method(rb_cDBM, "key", fsdbm_key, 1); rb_define_method(rb_cDBM, "select", fsdbm_select, 0); rb_define_method(rb_cDBM, "values_at", fsdbm_values_at, -1); rb_define_method(rb_cDBM, "length", fsdbm_length, 0); rb_define_method(rb_cDBM, "size", fsdbm_length, 0); rb_define_method(rb_cDBM, "empty?", fsdbm_empty_p, 0); rb_define_method(rb_cDBM, "each", fsdbm_each_pair, 0); rb_define_method(rb_cDBM, "each_value", fsdbm_each_value, 0); rb_define_method(rb_cDBM, "each_key", fsdbm_each_key, 0); rb_define_method(rb_cDBM, "each_pair", fsdbm_each_pair, 0); rb_define_method(rb_cDBM, "keys", fsdbm_keys, 0); rb_define_method(rb_cDBM, "values", fsdbm_values, 0); rb_define_method(rb_cDBM, "shift", fsdbm_shift, 0); rb_define_method(rb_cDBM, "delete", fsdbm_delete, 1); rb_define_method(rb_cDBM, "delete_if", fsdbm_delete_if, 0); rb_define_method(rb_cDBM, "reject!", fsdbm_delete_if, 0); rb_define_method(rb_cDBM, "reject", fsdbm_reject, 0); rb_define_method(rb_cDBM, "clear", fsdbm_clear, 0); rb_define_method(rb_cDBM,"invert", fsdbm_invert, 0); rb_define_method(rb_cDBM,"update", fsdbm_update, 1); rb_define_method(rb_cDBM,"replace", fsdbm_replace, 1); rb_define_method(rb_cDBM, "has_key?", fsdbm_has_key, 1); rb_define_method(rb_cDBM, "include?", fsdbm_has_key, 1); rb_define_method(rb_cDBM, "key?", fsdbm_has_key, 1); rb_define_method(rb_cDBM, "member?", fsdbm_has_key, 1); rb_define_method(rb_cDBM, "has_value?", fsdbm_has_value, 1); rb_define_method(rb_cDBM, "value?", fsdbm_has_value, 1); rb_define_method(rb_cDBM, "to_a", fsdbm_to_a, 0); rb_define_method(rb_cDBM, "to_hash", fsdbm_to_hash, 0); } sdbm-1.0.0/ext/sdbm/sdbm.h000066400000000000000000000043411321342345400152630ustar00rootroot00000000000000/* * sdbm - ndbm work-alike hashed database library * based on Per-Ake Larson's Dynamic Hashing algorithms. BIT 18 (1978). * author: oz@nexus.yorku.ca * status: public domain. */ #ifndef _SDBM_H_ #define _SDBM_H_ #include #define DBLKSIZ 4096 #define PBLKSIZ 1024 #define PAIRMAX 1008 /* arbitrary on PBLKSIZ-N */ #define SPLTMAX 10 /* maximum allowed splits */ /* for a single insertion */ #define DIRFEXT ".dir" #define PAGFEXT ".pag" typedef struct { int dirf; /* directory file descriptor */ int pagf; /* page file descriptor */ int flags; /* status/error flags, see below */ int keyptr; /* current key for nextkey */ off_t maxbno; /* size of dirfile in bits */ long curbit; /* current bit number */ long hmask; /* current hash mask */ long blkptr; /* current block for nextkey */ long blkno; /* current page to read/write */ long pagbno; /* current page in pagbuf */ char pagbuf[PBLKSIZ]; /* page file block buffer */ long dirbno; /* current block in dirbuf */ char dirbuf[DBLKSIZ]; /* directory file block buffer */ } DBM; #define DBM_RDONLY 0x1 /* data base open read-only */ #define DBM_IOERR 0x2 /* data base I/O error */ /* * utility macros */ #define sdbm_rdonly(db) ((db)->flags & DBM_RDONLY) #define sdbm_error(db) ((db)->flags & DBM_IOERR) #define sdbm_clearerr(db) ((db)->flags &= ~DBM_IOERR) /* ouch */ #define sdbm_dirfno(db) ((db)->dirf) #define sdbm_pagfno(db) ((db)->pagf) typedef struct { char *dptr; int dsize; } datum; extern datum nullitem; #if defined(__STDC__) #define proto(p) p #else #define proto(p) () #endif /* * flags to sdbm_store */ #define DBM_INSERT 0 #define DBM_REPLACE 1 /* * ndbm interface */ extern DBM *sdbm_open proto((char *, int, int)); extern void sdbm_close proto((DBM *)); extern datum sdbm_fetch proto((DBM *, datum)); extern int sdbm_delete proto((DBM *, datum)); extern int sdbm_store proto((DBM *, datum, datum, int)); extern datum sdbm_firstkey proto((DBM *)); extern datum sdbm_nextkey proto((DBM *)); /* * other */ extern DBM *sdbm_prep proto((char *, char *, int, int)); extern long sdbm_hash proto((char *, int)); #endif /* _SDBM_H_ */ sdbm-1.0.0/sdbm.gemspec000066400000000000000000000013511321342345400147300ustar00rootroot00000000000000# frozen_string_literal: true Gem::Specification.new do |s| s.name = "sdbm" s.version = '1.0.0' s.date = '2017-12-11' s.summary = "Provides a simple file-based key-value store with String keys and values." s.description = "Provides a simple file-based key-value store with String keys and values." s.require_path = %w{lib} s.files = %w{ext/sdbm/_sdbm.c ext/sdbm/extconf.rb ext/sdbm/init.c ext/sdbm/sdbm.h} s.extensions = ["ext/sdbm/extconf.rb"] s.required_ruby_version = ">= 2.3.0" s.authors = ["Yukihiro Matsumoto"] s.email = ["matz@ruby-lang.org"] s.homepage = "https://github.com/ruby/sdbm" s.license = "BSD-2-Clause" s.add_development_dependency "test-unit" s.add_development_dependency "rake-compiler" end sdbm-1.0.0/test/000077500000000000000000000000001321342345400134155ustar00rootroot00000000000000sdbm-1.0.0/test/sdbm/000077500000000000000000000000001321342345400143425ustar00rootroot00000000000000sdbm-1.0.0/test/sdbm/test_sdbm.rb000066400000000000000000000277771321342345400166770ustar00rootroot00000000000000# frozen_string_literal: false require 'test/unit' require 'tmpdir' begin require 'sdbm' rescue LoadError end class TestSDBM < Test::Unit::TestCase def setup @tmpdir = Dir.mktmpdir("tmptest_sdbm") @prefix = "tmptest_sdbm_#{$$}" @path = "#{@tmpdir}/#{@prefix}_" assert_instance_of(SDBM, @sdbm = SDBM.new(@path)) end def teardown assert_nil(@sdbm.close) ObjectSpace.each_object(SDBM) do |obj| obj.close unless obj.closed? end FileUtils.remove_entry_secure @tmpdir end def check_size(expect, sdbm=@sdbm) assert_equal(expect, sdbm.size) n = 0 sdbm.each { n+=1 } assert_equal(expect, n) if expect == 0 assert_equal(true, sdbm.empty?) else assert_equal(false, sdbm.empty?) end end def test_version assert(! SDBM.const_defined?(:VERSION)) end def test_s_new_has_no_block # SDBM.new ignore the block foo = true assert_instance_of(SDBM, sdbm = SDBM.new("#{@tmpdir}/#{@prefix}") { foo = false }) assert_equal(foo, true) assert_nil(sdbm.close) end def test_s_open_no_create assert_nil(sdbm = SDBM.open("#{@tmpdir}/#{@prefix}", nil)) ensure sdbm.close if sdbm end def test_s_open_with_block assert_equal(SDBM.open("#{@tmpdir}/#{@prefix}") { :foo }, :foo) end =begin # Is it guaranteed on many OS? def test_s_open_lock_one_process # locking on one process assert_instance_of(SDBM, sdbm = SDBM.open("#{@tmpdir}/#{@prefix}", 0644)) assert_raise(Errno::EWOULDBLOCK) { begin SDBM.open("#{@tmpdir}/#{@prefix}", 0644) rescue Errno::EAGAIN raise Errno::EWOULDBLOCK end } end =end def open_db_child(dbname, *opts) opts = [0644, *opts].map(&:inspect).join(', ') args = [EnvUtil.rubybin, "-rsdbm", <<-SRC, dbname] STDOUT.sync = true gdbm = SDBM.open(ARGV.shift, #{opts}) puts sdbm.class gets SRC IO.popen(args, "r+") do |f| dbclass = f.gets assert_equal("SDBM", dbclass.chomp) yield end end def test_s_open_nolock dbname = "#{@tmpdir}/#{@prefix}" open_db_child(dbname, SDBM::NOLOCK) do assert_no_exception(Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EACCES) { SDBM.open(dbname, 0644) {|sdbm| assert_instance_of(SDBM, sdbm) } } end p Dir.glob("#{@tmpdir}/#{@prefix}*") if $DEBUG open_db_child(dbname) do assert_no_exception(Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EACCES) { # this test is failed on Cygwin98 (???) SDBM.open(dbname, 0644, SDBM::NOLOCK) {|sdbm| assert_instance_of(SDBM, sdbm) } } end end if defined? SDBM::NOLOCK # sdbm 1.8.0 specific def test_s_open_error skip "doesn't support to avoid read access by owner on Windows" if /mswin|mingw/ =~ RUBY_PLATFORM assert_instance_of(SDBM, sdbm = SDBM.open("#{@tmpdir}/#{@prefix}", 0)) assert_raise(Errno::EACCES) { SDBM.open("#{@tmpdir}/#{@prefix}", 0) } sdbm.close end def test_close assert_instance_of(SDBM, sdbm = SDBM.open("#{@tmpdir}/#{@prefix}")) assert_nil(sdbm.close) # closed SDBM file assert_raise(SDBMError) { sdbm.close } end def test_aref assert_equal('bar', @sdbm['foo'] = 'bar') assert_equal('bar', @sdbm['foo']) assert_nil(@sdbm['bar']) end def test_fetch assert_equal('bar', @sdbm['foo']='bar') assert_equal('bar', @sdbm.fetch('foo')) # key not found assert_raise(IndexError) { @sdbm.fetch('bar') } # test for `ifnone' arg assert_equal('baz', @sdbm.fetch('bar', 'baz')) # test for `ifnone' block assert_equal('foobar', @sdbm.fetch('bar') {|key| 'foo' + key }) end def test_aset num = 0 2.times {|i| assert_equal('foo', @sdbm['foo'] = 'foo') assert_equal('foo', @sdbm['foo']) assert_equal('bar', @sdbm['foo'] = 'bar') assert_equal('bar', @sdbm['foo']) num += 1 if i == 0 assert_equal(num, @sdbm.size) # assign nil assert_equal('', @sdbm['bar'] = '') assert_equal('', @sdbm['bar']) num += 1 if i == 0 assert_equal(num, @sdbm.size) # empty string assert_equal('', @sdbm[''] = '') assert_equal('', @sdbm['']) num += 1 if i == 0 assert_equal(num, @sdbm.size) # Integer assert_equal('200', @sdbm['100'] = '200') assert_equal('200', @sdbm['100']) num += 1 if i == 0 assert_equal(num, @sdbm.size) # Big key and value assert_equal('y' * 100, @sdbm['x' * 100] = 'y' * 100) assert_equal('y' * 100, @sdbm['x' * 100]) num += 1 if i == 0 assert_equal(num, @sdbm.size) } end def test_key assert_equal('bar', @sdbm['foo'] = 'bar') assert_equal('foo', @sdbm.key('bar')) assert_nil(@sdbm['bar']) end def test_values_at keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values assert_equal(values.reverse, @sdbm.values_at(*keys.reverse)) end def test_select_with_block keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values ret = @sdbm.select {|k,v| assert_equal(k.upcase, v) k != "bar" } assert_equal([['baz', 'BAZ'], ['foo', 'FOO']], ret.sort) end def test_length num = 10 assert_equal(0, @sdbm.size) num.times {|i| i = i.to_s @sdbm[i] = i } assert_equal(num, @sdbm.size) @sdbm.shift assert_equal(num - 1, @sdbm.size) end def test_empty? assert_equal(true, @sdbm.empty?) @sdbm['foo'] = 'FOO' assert_equal(false, @sdbm.empty?) end def test_each_pair n = 0 @sdbm.each_pair { n += 1 } assert_equal(0, n) keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values n = 0 ret = @sdbm.each_pair {|key, val| assert_not_nil(i = keys.index(key)) assert_equal(val, values[i]) n += 1 } assert_equal(keys.size, n) assert_equal(@sdbm, ret) end def test_each_value n = 0 @sdbm.each_value { n += 1 } assert_equal(0, n) keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values n = 0 ret = @sdbm.each_value {|val| assert_not_nil(key = @sdbm.key(val)) assert_not_nil(i = keys.index(key)) assert_equal(val, values[i]) n += 1 } assert_equal(keys.size, n) assert_equal(@sdbm, ret) end def test_each_key n = 0 @sdbm.each_key { n += 1 } assert_equal(0, n) keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values n = 0 ret = @sdbm.each_key {|key| assert_not_nil(i = keys.index(key)) assert_equal(@sdbm[key], values[i]) n += 1 } assert_equal(keys.size, n) assert_equal(@sdbm, ret) end def test_keys assert_equal([], @sdbm.keys) keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values assert_equal(keys.sort, @sdbm.keys.sort) assert_equal(values.sort, @sdbm.values.sort) end def test_values test_keys end def test_shift assert_nil(@sdbm.shift) assert_equal(0, @sdbm.size) keys = %w(foo bar baz) values = %w(FOO BAR BAZ) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values ret_keys = [] ret_values = [] while ret = @sdbm.shift ret_keys.push ret[0] ret_values.push ret[1] assert_equal(keys.size - ret_keys.size, @sdbm.size) end assert_equal(keys.sort, ret_keys.sort) assert_equal(values.sort, ret_values.sort) end def test_delete keys = %w(foo bar baz) values = %w(FOO BAR BAZ) key = keys[1] assert_nil(@sdbm.delete(key)) assert_equal(0, @sdbm.size) @sdbm[keys[0]], @sdbm[keys[1]], @sdbm[keys[2]] = values assert_equal('BAR', @sdbm.delete(key)) assert_nil(@sdbm[key]) assert_equal(2, @sdbm.size) assert_nil(@sdbm.delete(key)) end def test_delete_with_block key = 'no called block' @sdbm[key] = 'foo' assert_equal('foo', @sdbm.delete(key) {|k| k.replace 'called block'; :blockval}) assert_equal(0, @sdbm.size) key = 'no called block' assert_equal(:blockval, @sdbm.delete(key) {|k| k.replace 'called block'; :blockval}) assert_equal(0, @sdbm.size) end def test_delete_if v = "0" 100.times {@sdbm[v] = v; v = v.next} ret = @sdbm.delete_if {|key, val| key.to_i < 50} assert_equal(@sdbm, ret) check_size(50, @sdbm) ret = @sdbm.delete_if {|key, val| key.to_i >= 50} assert_equal(@sdbm, ret) check_size(0, @sdbm) # break v = "0" 100.times {@sdbm[v] = v; v = v.next} check_size(100, @sdbm) n = 0; @sdbm.delete_if {|key, val| break if n > 50 n+=1 true } assert_equal(51, n) check_size(49, @sdbm) @sdbm.clear # raise v = "0" 100.times {@sdbm[v] = v; v = v.next} check_size(100, @sdbm) n = 0; begin @sdbm.delete_if {|key, val| raise "runtime error" if n > 50 n+=1 true } rescue RuntimeError end assert_equal(51, n) check_size(49, @sdbm) end def test_reject v = "0" 100.times {@sdbm[v] = v; v = v.next} hash = @sdbm.reject {|key, val| key.to_i < 50} assert_instance_of(Hash, hash) assert_equal(100, @sdbm.size) assert_equal(50, hash.size) hash.each_pair {|key,val| assert_equal(false, key.to_i < 50) assert_equal(key, val) } hash = @sdbm.reject {|key, val| key.to_i < 100} assert_instance_of(Hash, hash) assert_equal(true, hash.empty?) end def test_clear v = "1" 100.times {v = v.next; @sdbm[v] = v} assert_equal(@sdbm, @sdbm.clear) # validate SDBM#size i = 0 @sdbm.each { i += 1 } assert_equal(@sdbm.size, i) assert_equal(0, i) end def test_invert v = "0" 100.times {@sdbm[v] = v; v = v.next} hash = @sdbm.invert assert_instance_of(Hash, hash) assert_equal(100, hash.size) hash.each_pair {|key, val| assert_equal(key.to_i, val.to_i) } end def test_update hash = {} v = "0" 100.times {v = v.next; hash[v] = v} @sdbm["101"] = "101" @sdbm.update hash assert_equal(101, @sdbm.size) @sdbm.each_pair {|key, val| assert_equal(key.to_i, val.to_i) } end def test_replace hash = {} v = "0" 100.times {v = v.next; hash[v] = v} @sdbm["101"] = "101" @sdbm.replace hash assert_equal(100, @sdbm.size) @sdbm.each_pair {|key, val| assert_equal(key.to_i, val.to_i) } end def test_haskey? assert_equal('bar', @sdbm['foo']='bar') assert_equal(true, @sdbm.has_key?('foo')) assert_equal(false, @sdbm.has_key?('bar')) end def test_has_value? assert_equal('bar', @sdbm['foo']='bar') assert_equal(true, @sdbm.has_value?('bar')) assert_equal(false, @sdbm.has_value?('foo')) end def test_to_a v = "0" 100.times {v = v.next; @sdbm[v] = v} ary = @sdbm.to_a assert_instance_of(Array, ary) assert_equal(100, ary.size) ary.each {|key,val| assert_equal(key.to_i, val.to_i) } end def test_to_hash v = "0" 100.times {v = v.next; @sdbm[v] = v} hash = @sdbm.to_hash assert_instance_of(Hash, hash) assert_equal(100, hash.size) hash.each {|key,val| assert_equal(key.to_i, val.to_i) } end def test_closed assert_equal(false, @sdbm.closed?) @sdbm.close assert_equal(true, @sdbm.closed?) @sdbm = SDBM.new(@path) end def test_readonly @sdbm["bar"] = "baz" @sdbm.close File.chmod(0444, @path + ".dir") File.chmod(0444, @path + ".pag") @sdbm = SDBM.new(@path) assert_raise(SDBMError) { @sdbm["bar"] = "foo" } assert_raise(SDBMError) { @sdbm.delete("bar") } assert_raise(SDBMError) { @sdbm.delete_if { true } } assert_raise(SDBMError) { @sdbm.clear } assert_nil(@sdbm.store("bar", nil)) end def test_update2 obj = Object.new def obj.each_pair yield [] end assert_raise(ArgumentError) { @sdbm.update(obj) } end end if defined? SDBM