diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2021-10-09 13:15:48 +0300 |
---|---|---|
committer | Tomasz Konojacki <me@xenu.pl> | 2021-10-14 04:40:07 +0200 |
commit | 1d7b70436255bf0e7e5c9a8db29ffbf04371b978 (patch) | |
tree | f878d69bc5c27fce1586296fbb5554db3992c96f /ext | |
parent | 932ba9601963bae595e80f5dfab8cd1ae93f7ec2 (diff) | |
download | perl-1d7b70436255bf0e7e5c9a8db29ffbf04371b978.tar.gz |
GDBM_File: Implement crash-tolerance and export/import functions.
* ext/GDBM_File/Makefile.PL: Register new constants: gdbm_open
flags and return values for gdbm_latest_snapshot.
* ext/GDBM_File/GDBM_File.pm: Update documentation.
Export new constants.
Raise version to 1.21.
* ext/GDBM_File/GDBM_File.xs (dbcroak): Include system error
infomation, when appropriate.
(gdbm_syserrno): Return a meaningful value if not_here.
(gdbm_dump, gdbm_load, gdbm_convert)
(gdbm_failure_atomic, gdbm_latest_snapshot)
(gdbm_crash_tolerance_status): New functions.
* ext/GDBM_File/t/dump.t: New testcase.
* ext/GDBM_File/t/snapshot.t: New testcase.
* MANIFEST: List new files.
Diffstat (limited to 'ext')
-rw-r--r-- | ext/GDBM_File/GDBM_File.pm | 250 | ||||
-rw-r--r-- | ext/GDBM_File/GDBM_File.xs | 245 | ||||
-rw-r--r-- | ext/GDBM_File/Makefile.PL | 21 | ||||
-rw-r--r-- | ext/GDBM_File/t/dump.t | 101 | ||||
-rw-r--r-- | ext/GDBM_File/t/snapshot.t | 100 |
5 files changed, 659 insertions, 58 deletions
diff --git a/ext/GDBM_File/GDBM_File.pm b/ext/GDBM_File/GDBM_File.pm index a058b9d440..b002133a36 100644 --- a/ext/GDBM_File/GDBM_File.pm +++ b/ext/GDBM_File/GDBM_File.pm @@ -7,8 +7,8 @@ GDBM_File - Perl5 access to the gdbm library. =head1 SYNOPSIS use GDBM_File; - [$db =] tie %hash, 'GDBM_File', $filename, &GDBM_WRCREAT, 0640; - # Use the %hash array. + [$db =] tie %hash, 'GDBM_File', $filename, GDBM_WRCREAT, 0640; + # Use the %hash... $e = $db->errno; $e = $db->syserrno; @@ -53,9 +53,10 @@ GDBM_File - Perl5 access to the gdbm library. B<GDBM_File> is a module which allows Perl programs to make use of the facilities provided by the GNU gdbm library. If you intend to use this -module you should really have a copy of the gdbm manualpage at hand. +module you should really have a copy of the B<GDBM manual> at hand. +The manual is avaialble online at <https://www.gnu.org.ua/software/gdbm/manual>. -Most of the libgdbm.a functions are available through the GDBM_File +Most of the B<gdbm> functions are available through the B<GDBM_File> interface. Unlike Perl's built-in hashes, it is not safe to C<delete> the current @@ -112,8 +113,20 @@ The version is guaranteed to be not newer than B<I<MAJOR>.I<MINOR>>. $db->close; -Closes the database. You are not advised to use this method directly. Please, -use B<untie> instead. +Closes the database. Normally you would just do B<untie>. However, you +will need to use this function if you have explicitly assigned the result +of B<tie> to a variable, and you wish to release the database to another +users. Consider the following code: + + $db = tie %hash, 'GDBM_File', $filename, GDBM_WRCREAT, 0640; + # Do something with %hash or $db... + untie %hash; + $db->close; + +In this example, doing B<untie> or alone is not enough, since the database +would remain referenced by B<$db>, and, as a consequence, the database file +would remain locked. Calling B<$db->close> ensures the database file is +closed and unlocked. =head2 errno @@ -305,6 +318,200 @@ Number of buckets that failed to be retrieved. =back +=head2 convert + + $db->convert($format); + +Changes the format of the database file referred to by B<$db>. + +Starting from version 1.20, B<gdbm> supports two database file formats: +I<standard> and I<extended>. The former is the traditional database +format, used by previous B<gdbm> versions. The I<extended> format contains +additional data and is recommended for use in crash tolerant applications. + +L<https://www.gnu.org.ua/software/gdbm/manual/Numsync.html>, for the +discussion of both formats. + +The B<$format> argument sets the new desired database format. It is +B<GDBM_NUMSYNC> to convert the database from standard to extended format, and +B<0> to convert it from extended to standard format. + +If the database is already in the requested format, the function returns +success without doing anything. + +=head2 dump + + $db->dump($filename, %options) + +Creates a dump of the database file in I<$filename>. Such file can be used +as a backup copy or sent over a wire to recreate the database on another +machine. To create a database from the dump file, use the B<load> method. + +B<GDBM> supports two dump formats: old I<binary> and new I<ascii>. The +binary format is not portable across architectures and is deprecated. It +is supported for backward compatibility. The ascii format is portable and +stores additional meta-data about the file. It was introduced with the +B<gdbm> version 1.11 and is the preferred dump format. The B<dump> method +creates ascii dumps by default. + +If the named file already exists, the function will refuse to overwrite and +will croak an error. If it doesn't exist, it will be created with the +mode B<0666> modified by the current B<umask>. + +These defaults can be altered using the following I<%options>: + +=over 4 + +=item B<binary> => 1 + +Create dump in I<binary> format. + +=item B<mode> => I<MODE> + +Set file mode to I<MODE>. + +=item B<overwrite> => 1 + +Silently overwrite existing files. + +=back + +=head2 load + + $db->load($filename, %options) + +Load the data from the dump file I<$filename> into the database I<$db>. +The file must have been previously created using the B<dump> method. File +format is recognized automatically. By default, the function will croak +if the dump contains a key that already exists in the database. It will +silently ignore the failure to restore database mode and/or ownership. +These defaults can be altered using the following I<%options>: + +=over 4 + +=item B<replace> => 1 + +Replace existing keys. + +=item B<restore_mode> => 0 | 1 + +If I<0>, don't try to restore the mode of the database file to that stored +in the dump. + +=item B<restore_owner> => 0 | 1 + +If I<0>, don't try to restore the owner of the database file to that stored +in the dump. + +=item B<strict_errors> => 1 + +Croak if failed to restore ownership and/or mode. + +=back + +The usual sequence to recreate a database from the dump file is: + + my %hash; + my $db = tie %hash, 'GDBM_File', 'a.db', GDBM_NEWDB, 0640; + $db->load('a.dump'); + +=head1 CRASH TOLERANCE + +Crash tolerance is a new feature that, given appropriate support from the OS +and the filesystem, guarantees that a logically consistent recent state of the +database can be recovered following a crash, such as power outage, OS kernel +panic, or the like. + +Crash tolerance support appeared in B<gdbm> version 1.21. The theory behind +it is explained in "Crashproofing the Original NoSQL Key-Value Store", +by Terence Kelly (L<https://queue.acm.org/detail.cfm?id=3487353>). A +detailed discussion of the B<gdbm> implementation is available in the +B<GDBM Manual> (L<https://www.gnu.org.ua/software/gdbm/manual/Crash-Tolerance.html>). The information below describes the Perl interface. + +For maximum robustness, we recommend to use I<extended database format> +for crash tolerant databases. To create a database in extended format, +use the B<GDBM_NEWDB|GDBM_NUMSYNC> when opening the database, e.g.: + + $db = tie %hash, 'GDBM_File', $filename, + GDBM_NEWDB|GDBM_NUMSYNC, 0640; + +To convert existing database to the extended format, use the B<convert> +method, described above, e.g.: + + $db->convert(GDBM_NUMSYNC); + +=head2 crash_tolerance_status + + GDBM_File->crash_tolerance_status; + +This static method returns the status of crash tolerance support. A +non-zero value means crash tolerance is compiled in and supported by +the operating system. + +=head2 failure_atomic + + $db->failure_atomic($even, $odd) + +Enables crash tolerance for the database B<$db>, Arguments are +the pathnames of two files that will be created and filled with +I<snapshots> of the database file. The two files must not exist +when this method is called and must reside on the same filesystem +as the database file. This filesystem must be support the I<reflink> +operation (https://www.gnu.org.ua/software/gdbm/manual/Filesystems-supporting-crash-tolerance.html>. + +After a successful call to B<failure_atomic>, every call to B<$db->sync> +method will make an efficient reflink snapshot of the database file in +one of these files; consecutive calls to B<sync> alternate between the +two, hence the names. + +The most recent of these files can be used to recover the database after +a crash. To select the right snapshot, use the B<latest_snapshot> +static method. + +=head2 latest_snapshot + + $file = GDBM_File->latest_snapshot($even, $odd); + + ($file, $error) = GDBM_File->latest_snapshot($even, $odd); + +Given the two snapshot names (the ones used previously in a call to +B<failure_atomic>), this method selects the one suitable for database +recovery, i.e. the file which contains the most recent database snapshot. + +In scalar context, it returns the selected file name or B<undef> in case +of failure. + +In array context, the returns a list of two elements: the file name +and status code. On success, the file name is defined and the code +is B<GDBM_SNAPSHOT_OK>. On error, the file name is B<undef>, and +the status is one of the following: + +=over 4 + +=item GDBM_SNAPSHOT_BAD + +Neither snapshot file is applicable. This means that the crash has occurred +before a call to B<failure_atomic> completed. In this case, it is best to +fall back on a safe backup copy of the data file. + +=item GDBM_SNAPSHOT_ERR + +A system error occurred. Examine B<$!> for details. See +<https://www.gnu.org.ua/software/gdbm/manual/Crash-recovery.html> for +a comprehensive list of error codes and their meaning. + +=item GDBM_SNAPSHOT_SAME + +The file modes and modification dates of both snapshot files are exactly the +same. This can happen only for databases in standard format. + +=item GDBM_SNAPSHOT_SUSPICIOUS + +The I<numsync> counters of the two snapshots differ by more than one. The +most probable reason is programmer's error: the two parameters refer to +snapshots belonging to different database files. + +=back =head1 AVAILABILITY @@ -315,15 +522,19 @@ L<http://www.gnu.org/order/ftp.html>. =head1 SECURITY AND PORTABILITY -B<Do not accept GDBM files from untrusted sources.> - -GDBM files are not portable across platforms. +GDBM files are not portable across platforms. If you wish to transfer +a GDBM file over the wire, dump it to a portable format first. -The GDBM documentation doesn't imply that files from untrusted sources -can be safely used with C<libgdbm>. +B<Do not accept GDBM files from untrusted sources.> -A maliciously crafted file might cause perl to crash or even expose a -security vulnerability. +Robustness of GDBM against corrupted databases depends highly on its +version. Versions prior to 1.15 did not implement any validity +checking, so that a corrupted or maliciously crafted database file +could cause perl to crash or even expose a security vulnerability. +Versions between 1.15 and 1.20 were progressively strengthened against +invalid inputs. Finally, version 1.21 had undergone extensive fuzzy +checking which proved its ability to withstand any kinds of inputs +without crashing. =head1 SEE ALSO @@ -360,10 +571,21 @@ require XSLoader; GDBM_SYNCMODE GDBM_WRCREAT GDBM_WRITER + GDBM_NOMMAP + GDBM_CLOEXEC + GDBM_BSEXACT + GDBM_XVERIFY + GDBM_PREREAD + GDBM_NUMSYNC + GDBM_SNAPSHOT_OK + GDBM_SNAPSHOT_BAD + GDBM_SNAPSHOT_ERR + GDBM_SNAPSHOT_SAME + GDBM_SNAPSHOT_SUSPICIOUS ); # This module isn't dual life, so no need for dev version numbers. -$VERSION = '1.20'; +$VERSION = '1.21'; XSLoader::load(); diff --git a/ext/GDBM_File/GDBM_File.xs b/ext/GDBM_File/GDBM_File.xs index 338d51c2a6..a0ce12c498 100644 --- a/ext/GDBM_File/GDBM_File.xs +++ b/ext/GDBM_File/GDBM_File.xs @@ -163,12 +163,15 @@ output_datum(pTHX_ SV *arg, char *str, int size) static void dbcroak(GDBM_File db, char const *func) { -#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 13 - croak("%s: %s", func, gdbm_db_strerror(db->dbp)); +#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 13 + if (db) + croak("%s: %s", func, gdbm_db_strerror(db->dbp)); + if (gdbm_check_syserr(gdbm_errno)) + croak("%s: %s: %s", func, gdbm_strerror(gdbm_errno), strerror(errno)); #else (void)db; - croak("%s: %s", func, gdbm_strerror(gdbm_errno)); #endif + croak("%s: %s", func, gdbm_strerror(gdbm_errno)); } #if GDBM_VERSION_MAJOR == 1 && (GDBM_VERSION_MINOR > 16 || GDBM_VERSION_PATCH >= 90) @@ -258,9 +261,9 @@ gdbm_TIEHASH(dbtype, name, read_write, mode) char * name int read_write int mode - PREINIT: + PREINIT: GDBM_FILE dbp; - CODE: + CODE: dbp = gdbm_open(name, 0, read_write, mode, FATALFUNC); if (!dbp && gdbm_errno == GDBM_BLOCK_SIZE_ERROR) { /* @@ -280,13 +283,13 @@ gdbm_TIEHASH(dbtype, name, read_write, mode) } else { RETVAL = NULL; } - OUTPUT: + OUTPUT: RETVAL void gdbm_DESTROY(db) GDBM_File db - PREINIT: + PREINIT: int i = store_value; CODE: if (gdbm_file_close(db)) { @@ -423,7 +426,7 @@ gdbm_syserrno(db) } } #else - not_here("syserrno"); + RETVAL = not_here("syserrno"); #endif OUTPUT: RETVAL @@ -499,17 +502,16 @@ gdbm_recover(db, ...) if (items > 1) { int i; if ((items % 2) == 0) { - croak("bad number of arguments"); + croak_xs_usage(cv, "db, %opts"); } for (i = 1; i < items; i += 2) { char *kw; SV *sv = ST(i); SV *val = ST(i+1); - if (!SvPOK(sv)) - croak("bad arguments near #%d", i); kw = SvPV_nolen(sv); if (strcmp(kw, "err") == 0) { + SvGETMAGIC(val); if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVCV) { rcvr.data = SvRV(val); } else { @@ -518,38 +520,28 @@ gdbm_recover(db, ...) rcvr.errfun = rcvr_errfun; flags |= GDBM_RCVR_ERRFUN; } else if (strcmp(kw, "max_failed_keys") == 0) { - if (SvIOK(val)) { - rcvr.max_failed_keys = SvUV(val); - } else { - croak("max_failed_keys must be numeric"); - } + rcvr.max_failed_keys = SvUV(val); flags |= GDBM_RCVR_MAX_FAILED_KEYS; } else if (strcmp(kw, "max_failed_buckets") == 0) { - if (SvIOK(val)) { - rcvr.max_failed_buckets = SvUV(val); - } else { - croak("max_failed_buckets must be numeric"); - } + rcvr.max_failed_buckets = SvUV(val); flags |= GDBM_RCVR_MAX_FAILED_BUCKETS; } else if (strcmp(kw, "max_failures") == 0) { - if (SvIOK(val)) { - rcvr.max_failures = SvUV(val); - } else { - croak("max_failures must be numeric"); - } + rcvr.max_failures = SvUV(val); flags |= GDBM_RCVR_MAX_FAILURES; } else if (strcmp(kw, "backup") == 0) { + SvGETMAGIC(val); if (SvROK(val) && SvTYPE(SvRV(val)) < SVt_PVAV) { backup_ref = val; } else { - croak("backup must be a scalar reference"); + croak("%s must be a scalar reference", kw); } flags |= GDBM_RCVR_BACKUP; } else if (strcmp(kw, "stat") == 0) { + SvGETMAGIC(val); if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) { stat_ref = val; } else { - croak("backup must be a scalar reference"); + croak("%s must be a scalar reference", kw); } } else { croak("%s: unrecognized argument", kw); @@ -613,7 +605,7 @@ gdbm_count_t gdbm_count(db) GDBM_File db PREINIT: - gdbm_count_t c; + gdbm_count_t c; INIT: CHECKDB(db); CODE: @@ -624,6 +616,110 @@ gdbm_count(db) OUTPUT: RETVAL +void +gdbm_dump(db, filename, ...) + GDBM_File db + char * filename + PREINIT: + int format = GDBM_DUMP_FMT_ASCII; + int flags = GDBM_WRCREAT; + int mode = 0666; + INIT: + CHECKDB(db); + CODE: + if (items % 2) { + croak_xs_usage(cv, "db, filename, %opts"); + } else { + int i; + + for (i = 2; i < items; i += 2) { + char *kw; + SV *sv = ST(i); + SV *val = ST(i+1); + + kw = SvPV_nolen(sv); + if (strcmp(kw, "mode") == 0) { + mode = SvUV(val) & 0777; + } else if (strcmp(kw, "binary") == 0) { + if (SvTRUE(val)) { + format = GDBM_DUMP_FMT_BINARY; + } + } else if (strcmp(kw, "overwrite") == 0) { + if (SvTRUE(val)) { + flags = GDBM_NEWDB; + } + } else { + croak("unrecognized keyword: %s", kw); + } + } + if (gdbm_dump(db->dbp, filename, format, flags, mode)) { + dbcroak(NULL, "dump"); + } + } + +void +gdbm_load(db, filename, ...) + GDBM_File db + char * filename + PREINIT: + int flag = GDBM_INSERT; + int meta_mask = 0; + unsigned long errline; + int result; + int strict_errors = 0; + INIT: + CHECKDB(db); + CODE: + if (items % 2) { + croak_xs_usage(cv, "db, filename, %opts"); + } else { + int i; + + for (i = 2; i < items; i += 2) { + char *kw; + SV *sv = ST(i); + SV *val = ST(i+1); + + kw = SvPV_nolen(sv); + + if (strcmp(kw, "restore_mode") == 0) { + if (!SvTRUE(val)) + meta_mask |= GDBM_META_MASK_MODE; + } else if (strcmp(kw, "restore_owner") == 0) { + if (!SvTRUE(val)) + meta_mask |= GDBM_META_MASK_OWNER; + } else if (strcmp(kw, "replace") == 0) { + if (SvTRUE(val)) + flag = GDBM_REPLACE; + } else if (strcmp(kw, "strict_errors") == 0) { + strict_errors = SvTRUE(val); + } else { + croak("unrecognized keyword: %s", kw); + } + } + } + + result = gdbm_load(&db->dbp, filename, flag, meta_mask, &errline); + if (result == -1 || (result == 1 && strict_errors)) { +#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 13 + if (errline) { + croak("%s:%lu: database load error: %s", + filename, errline, gdbm_db_strerror(db->dbp)); + } else { + croak("%s: database load error: %s", + filename, gdbm_db_strerror(db->dbp)); + } +#else + if (errline) { + croak("%s:%lu: database load error: %s", + filename, errline, gdbm_strerror(gdbm_errno)); + } else { + croak("%s: database load error: %s", + filename, gdbm_strerror(gdbm_errno)); + } +#endif + } + #endif #define OPTNAME(a,b) a ## b @@ -633,11 +729,7 @@ gdbm_count(db) opcode = OPTNAME(GDBM_GET, opt); \ } else { \ opcode = OPTNAME(GDBM_SET, opt); \ - sv = ST(1); \ - if (!SvIOK(sv)) { \ - croak("%s: bad argument type", opt_names[ix]); \ - } \ - c_iv = SvIV(sv); \ + c_iv = SvIV(ST(1)); \ } \ } while (0) @@ -725,7 +817,6 @@ gdbm_flags(db, ...) char *c_cv; OPTVALPTR vptr = (OPTVALPTR) &c_iv; size_t vsiz = sizeof(c_iv); - SV *sv; INIT: CHECKDB(db); CODE: @@ -779,11 +870,7 @@ gdbm_flags(db, ...) opcode = GDBM_GETMAXMAPSIZE; } else { opcode = GDBM_SETMAXMAPSIZE; - sv = ST(1); - if (!SvUOK(sv)) { - croak("%s: bad argument type", opt_names[ix]); - } - c_uv = SvUV(sv); + c_uv = SvUV(ST(1)); } break; } @@ -831,4 +918,82 @@ filter_fetch_key(db, code) GDBM_File::filter_store_value = store_value CODE: DBM_setFilter(db->filter[ix], code); + +# +# Export/Import API +# + + +# +# Crash tolerance API +# + +#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 21 + +#define gdbm_convert(db, flag) gdbm_convert(db->dbp, flag) +int +gdbm_convert(db, flag) + GDBM_File db + int flag + INIT: + CHECKDB(db); + CLEANUP: + if (RETVAL) { + dbcroak(db, "gdbm_convert"); + } + +#define gdbm_failure_atomic(db, even, odd) gdbm_failure_atomic(db->dbp, even, odd) + +int +gdbm_failure_atomic(db, even, odd) + GDBM_File db + char * even + char * odd + INIT: + CHECKDB(db); + CLEANUP: + if (RETVAL) { + dbcroak(db, "gdbm_failure_atomic"); + } + +void +gdbm_latest_snapshot(package, even, odd) + char * even + char * odd + INIT: + int result; + int syserr; + const char * filename; + PPCODE: + result = gdbm_latest_snapshot(even, odd, &filename); + syserr = errno; + if (result == GDBM_SNAPSHOT_OK) { + XPUSHs(sv_2mortal(newSVpv(filename, 0))); + } else { + XPUSHs(&PL_sv_undef); + } + if (GIMME_V == G_ARRAY) { + XPUSHs(sv_2mortal(newSVuv(result))); + if (result == GDBM_SNAPSHOT_ERR) + XPUSHs(sv_2mortal(newSVuv(syserr))); + } + +#endif + +int +gdbm_crash_tolerance_status(package) + CODE: +#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 21 + /* + * The call below returns GDBM_SNAPSHOT_ERR and sets errno to + * EINVAL, if crash tolerance is implemented, or ENOSYS, if it + * is not. + */ + gdbm_latest_snapshot(NULL, NULL, NULL); + RETVAL = (errno != ENOSYS); +#else + RETVAL = 0; +#endif + OUTPUT: + RETVAL diff --git a/ext/GDBM_File/Makefile.PL b/ext/GDBM_File/Makefile.PL index 2a44d3e1d8..23b4760a76 100644 --- a/ext/GDBM_File/Makefile.PL +++ b/ext/GDBM_File/Makefile.PL @@ -8,13 +8,26 @@ WriteMakefile( realclean => {FILES=> 'const-c.inc const-xs.inc'}, XS_VERSION => eval MM->parse_version('GDBM_File.pm'), #silence warnings if we are a dev release ); + +my @names = qw(GDBM_CACHESIZE GDBM_CENTFREE GDBM_COALESCEBLKS + GDBM_FAST GDBM_FASTMODE GDBM_INSERT GDBM_NEWDB GDBM_NOLOCK + GDBM_OPENMASK GDBM_READER GDBM_REPLACE GDBM_SYNC GDBM_SYNCMODE + GDBM_WRCREAT GDBM_WRITER GDBM_NOMMAP GDBM_CLOEXEC GDBM_BSEXACT + GDBM_XVERIFY GDBM_PREREAD GDBM_NUMSYNC); + +push @names, { + name => $_, + type => "IV", + macro => [ "#if GDBM_VERSION_MAJOR == 1 && GDBM_VERSION_MINOR >= 21\n", + "#endif\n" ], + value => "$_", +} foreach qw(GDBM_SNAPSHOT_OK GDBM_SNAPSHOT_BAD GDBM_SNAPSHOT_ERR + GDBM_SNAPSHOT_SAME GDBM_SNAPSHOT_SUSPICIOUS); + WriteConstants( NAME => 'GDBM_File', DEFAULT_TYPE => 'IV', BREAKOUT_AT => 8, PROXYSUBS => {autoload => 1}, - NAMES => [qw(GDBM_CACHESIZE GDBM_CENTFREE GDBM_COALESCEBLKS - GDBM_FAST GDBM_FASTMODE GDBM_INSERT GDBM_NEWDB GDBM_NOLOCK - GDBM_OPENMASK GDBM_READER GDBM_REPLACE GDBM_SYNC GDBM_SYNCMODE - GDBM_WRCREAT GDBM_WRITER)], + NAMES => \@names ); diff --git a/ext/GDBM_File/t/dump.t b/ext/GDBM_File/t/dump.t new file mode 100644 index 0000000000..1547774cca --- /dev/null +++ b/ext/GDBM_File/t/dump.t @@ -0,0 +1,101 @@ +#!./perl -w +use strict; + +use Test::More; +use Config; +use File::Temp 'tempdir'; +use File::Spec; +use Fcntl qw( :mode ); + +BEGIN { + plan(skip_all => "GDBM_File was not built") + unless $Config{extensions} =~ /\bGDBM_File\b/; + + plan(tests => 18); + use_ok('GDBM_File'); +} + +use constant { + DUMP_ASCII => 0, + DUMP_BIN => 1, + DUMP_UNKNOWN => -1 +}; + +sub dump_format { + my $file = shift; + if (open(my $fd, '<', $file)) { + $_ = <$fd>; + if (/^# GDBM dump file created by GDBM version/) { + return DUMP_ASCII; + } + if (/^!\r$/) { + $_ = <$fd>; + if (/^! GDBM FLAT FILE DUMP -- THIS IS NOT A TEXT FILE/) { + return DUMP_BIN; + } + } + } + return DUMP_UNKNOWN; +} + +my $wd = tempdir(CLEANUP => 1); +my $dbname = File::Spec->catfile($wd, 'Op_dbmx'); +my %h; +my $db = tie(%h, 'GDBM_File', $dbname, GDBM_WRCREAT, 0640); +isa_ok($db, 'GDBM_File'); +SKIP: { + skip 'GDBM_File::dump not available', 16 + unless $db->can('dump'); + + $h{one} = '1'; + $h{two} = '2'; + $h{three} = '3'; + + my $dumpname = "$dbname.dump"; + is(eval { $db->dump($dumpname); 1 }, 1, "Create ASCII dump"); + is(dump_format($dumpname), DUMP_ASCII, "ASCII dump created"); + is(eval { $db->dump($dumpname); 1 }, undef, "Refuse to overwrite existing dump"); + is(eval { $db->dump($dumpname, overwrite => 1); 1 }, 1, "Working overwrite option"); + + my $binname = "$dbname.bin"; + is(eval { $db->dump($binname, binary => 1); 1 }, 1, "Create binary dump"); + is(dump_format($binname), DUMP_BIN, "Binary dump created"); + is(eval { $db->dump($binname, binary => 1); 1 }, undef, "Refuse to overwrite existing binary dump"); + is(eval { $db->dump($binname, binary => 1, overwrite => 1); 1 }, 1, "Working overwrite option (binary format)"); + + untie %h; + $db->close; + + # + # Test loading the database + # + + $db = tie(%h, 'GDBM_File', $dbname, GDBM_NEWDB, 0640); + isa_ok($db, 'GDBM_File'); + + is(eval { $db->load($dumpname); 1 }, 1, "Loading from ascii dump"); + is_deeply({map { $_ => $h{$_} } sort keys %h}, + { one => 1, two => 2, three => 3 }, + "Restored database content"); + + is(eval { $db->load($dumpname); 1 }, undef, "Refuse to replace existing keys"); + + is(eval { $db->load($dumpname, replace => 1); 1 }, 1, "Replace existing keys"); + + untie %h; + $db->close; + + # + # Test loading the database from binary dump + # + $db = tie(%h, 'GDBM_File', $dbname, GDBM_NEWDB, 0640); + isa_ok($db, 'GDBM_File'); + + is(eval { $db->load($binname); 1 }, 1, "Loading from binary dump"); + is_deeply({map { $_ => $h{$_} } sort keys %h}, + { one => 1, two => 2, three => 3 }, + "Restored database content"); + +} + + diff --git a/ext/GDBM_File/t/snapshot.t b/ext/GDBM_File/t/snapshot.t new file mode 100644 index 0000000000..3f0af8133b --- /dev/null +++ b/ext/GDBM_File/t/snapshot.t @@ -0,0 +1,100 @@ +#!./perl -w +use strict; + +use Test::More; +use Config; +use File::Temp 'tempdir'; +use File::Spec; +use Fcntl qw( :mode ); + +BEGIN { + plan(skip_all => "GDBM_File was not built") + unless $Config{extensions} =~ /\bGDBM_File\b/; + + plan(tests => 7); + use_ok('GDBM_File'); +} + +SKIP: { + skip "GDBM_File crash tolerance not available", 6, + unless GDBM_File->crash_tolerance_status; + + my $wd = tempdir(CLEANUP => 1); + chdir $wd; + + sub createdb { + my ($name, $type, $code) = @_; + my %h; + $type //= 0; + my $dbh = tie(%h, 'GDBM_File', $name, GDBM_NEWDB|$type, 0640); + if ($code) { + &{$code}($dbh, \%h); + } + untie %h + } + my $even = 'a.db'; + my $odd = 'b.db'; + my $time = time; + + # + # Valid cases + # + + # access modes + createdb($even); + createdb($odd); + chmod S_IRUSR, $even; + chmod S_IWUSR, $odd; + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ 'a.db', GDBM_SNAPSHOT_OK ], "different acess modes"); + + # mtimes + chmod S_IRUSR, $odd; + utime($time, $time, $even); + utime($time, $time-5, $odd); + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ 'a.db', GDBM_SNAPSHOT_OK ], "different mtimes"); + unlink $even, $odd; + + # numsync + createdb($even, GDBM_NUMSYNC); + createdb($odd, GDBM_NUMSYNC, sub { shift->sync }); + chmod S_IRUSR, $even, $odd; + utime($time, $time, $even, $odd); + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ 'b.db', GDBM_SNAPSHOT_OK ], "different numsync value"); + + # + # Erroneous cases + # + + unlink $even, $odd; + + # Same snapshots + createdb($even); + createdb($odd); + chmod S_IRUSR, $even, $odd; + utime($time, $time, $even, $odd); + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ undef, GDBM_SNAPSHOT_SAME ], "GDBM_SNAPSHOT_SAME"); + + # Both writable + chmod S_IWUSR, $even, $odd; + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ undef, GDBM_SNAPSHOT_BAD ], "GDBM_SNAPSHOT_BAD"); + + # numsync difference > 1 + unlink $even, $odd; + + createdb($even, GDBM_NUMSYNC); + createdb($odd, GDBM_NUMSYNC, + sub { + my $dbh = shift; + $dbh->sync; + $dbh->sync; + }); + chmod S_IRUSR, $even, $odd; + utime($time, $time, $even, $odd); + is_deeply([GDBM_File->latest_snapshot($even, $odd)], + [ undef, GDBM_SNAPSHOT_SUSPICIOUS ], "GDBM_SNAPSHOT_SUSPICIOUS"); +} |