summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org>2021-10-09 13:15:48 +0300
committerTomasz Konojacki <me@xenu.pl>2021-10-14 04:40:07 +0200
commit1d7b70436255bf0e7e5c9a8db29ffbf04371b978 (patch)
treef878d69bc5c27fce1586296fbb5554db3992c96f /ext
parent932ba9601963bae595e80f5dfab8cd1ae93f7ec2 (diff)
downloadperl-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.pm250
-rw-r--r--ext/GDBM_File/GDBM_File.xs245
-rw-r--r--ext/GDBM_File/Makefile.PL21
-rw-r--r--ext/GDBM_File/t/dump.t101
-rw-r--r--ext/GDBM_File/t/snapshot.t100
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");
+}