]> Dogcows Code - chaz/p5-File-KDBX/commitdiff
Don't open already-open files on Windows
authorCharles McGarvey <ccm@cpan.org>
Mon, 2 May 2022 07:06:36 +0000 (01:06 -0600)
committerCharles McGarvey <ccm@cpan.org>
Mon, 2 May 2022 07:15:33 +0000 (01:15 -0600)
Changes
lib/File/KDBX.pm
lib/File/KDBX/Dumper.pm
lib/File/KDBX/Key/File.pm
lib/File/KDBX/Loader.pm
lib/File/KDBX/Util.pm
t/database.t
t/hash-block.t
t/hmac-block.t
t/keys.t

diff --git a/Changes b/Changes
index 1f54ece58d42c54182d57787da9be101214c9dc3..0debc4fe928c531f8327a2ba01e76b4627befa60 100644 (file)
--- a/Changes
+++ b/Changes
@@ -2,8 +2,10 @@ Revision history for File-KDBX.
 
 {{$NEXT}}
 
-  * Fix a bug where peeking at memory-protected strings and binaries does not
-    work without unlocking the database at least once.
+  * Fixed a bug where peeking at memory-protected strings and binaries does
+    not work without unlocking the database at least once.
+  * Added an option for writing files non-atomically.
+  * Fixed broken tests on Windows.
 
 0.900     2022-05-01 12:55:59-0600
 
@@ -12,7 +14,7 @@ Revision history for File-KDBX.
   * Now use the database maintenance_history_days value as the default
     "max_age" value in prune_history method.
   * Fixed distribution prereq issues.
-  * Clean up a lot of pod typos and other inaccuracies.
+  * Cleaned up a lot of pod typos and other inaccuracies.
 
 0.800     2022-04-30 21:14:30-0600
 
index 255958afb60f92123b73089d5dfcbce71bbdcf3a..4768eefa4108a056c40cce46e1d57bc8163f2994 100644 (file)
@@ -1427,8 +1427,8 @@ sub randomize_seeds {
     $key = $kdbx->key($primitive);
 
 Get or set a L<File::KDBX::Key>. This is the master key (e.g. a password or a key file that can decrypt
-a database). You can also pass a primitive that can be cast to a B<Key>. See L<File::KDBX::Key/new> for an
-explanation of what the primitive can be.
+a database). You can also pass a primitive castable to a B<Key>. See L<File::KDBX::Key/new> for an explanation
+of what the primitive can be.
 
 You generally don't need to call this directly because you can provide the key directly to the loader or
 dumper when loading or dumping a KDBX file.
@@ -2419,12 +2419,12 @@ B<TODO> - This is a planned feature, not yet implemented.
 =head1 ERRORS
 
 Errors in this package are constructed as L<File::KDBX::Error> objects and propagated using perl's built-in
-mechanisms. Fatal errors are propagated using L<functions/die> and non-fatal errors (a.k.a. warnings) are
-propagated using L<functions/warn> while adhering to perl's L<warnings> system. If you're already familiar
-with these mechanisms, you can skip this section.
+mechanisms. Fatal errors are propagated using L<perlfunc/"die LIST"> and non-fatal errors (a.k.a. warnings)
+are propagated using L<perlfunc/"warn LIST"> while adhering to perl's L<warnings> system. If you're already
+familiar with these mechanisms, you can skip this section.
 
-You can catch fatal errors using L<functions/eval> (or something like L<Try::Tiny>) and non-fatal errors using
-C<$SIG{__WARN__}> (see L<variables/%SIG>). Examples:
+You can catch fatal errors using L<perlfunc/"eval BLOCK"> (or something like L<Try::Tiny>) and non-fatal
+errors using C<$SIG{__WARN__}> (see L<perlvar/%SIG>). Examples:
 
     use File::KDBX::Error qw(error);
 
index 6f8d8bb120936ab361606df3bf8157ac171c4f49..39d4782b0b842ca8b0744730105d775d266cd96b 100644 (file)
@@ -105,13 +105,23 @@ sub reset {
 
 =method dump
 
-    $dumper->dump(\$string, $key);
-    $dumper->dump(*IO, $key);
-    $dumper->dump($filepath, $key);
+    $dumper->dump(\$string, %options);
+    $dumper->dump(\$string, $key, %options);
+    $dumper->dump(*IO, %options);
+    $dumper->dump(*IO, $key, %options);
+    $dumper->dump($filepath, %options);
+    $dumper->dump($filepath, $key, %options);
 
 Dump a KDBX file.
 
-The C<$key> is either a L<File::KDBX::Key> or a primitive that can be cast to a Key object.
+The C<$key> is either a L<File::KDBX::Key> or a primitive castable to a Key object. Available options:
+
+=for :list
+* C<kdbx> - Database to dump (default: value of L</kdbx>)
+* C<key> - Alternative way to specify C<$key> (default: value of L</File::KDBX/key>)
+
+Other options are supported depending on the first argument. See L</dump_string>, L</dump_file> and
+L</dump_handle>.
 
 =cut
 
@@ -126,10 +136,16 @@ sub dump {
 
 =method dump_string
 
-    $dumper->dump_string(\$string, $key);
-    \$string = $dumper->dump_string($key);
+    $dumper->dump_string(\$string, %options);
+    $dumper->dump_string(\$string, $key, %options);
+    \$string = $dumper->dump_string(%options);
+    \$string = $dumper->dump_string($key, %options);
+
+Dump a KDBX file to a string / memory buffer. Available options:
 
-Dump a KDBX file to a string / memory buffer.
+=for :list
+* C<kdbx> - Database to dump (default: value of L</kdbx>)
+* C<key> - Alternative way to specify C<$key> (default: value of L</File::KDBX/key>)
 
 =cut
 
@@ -156,9 +172,18 @@ sub dump_string {
 
 =method dump_file
 
-    $dumper->dump_file($filepath, $key);
+    $dumper->dump_file($filepath, %options);
+    $dumper->dump_file($filepath, $key, %options);
+
+Dump a KDBX file to a filesystem. Available options:
 
-Dump a KDBX file to a filesystem.
+=for :list
+* C<kdbx> - Database to dump (default: value of L</kdbx>)
+* C<key> - Alternative way to specify C<$key> (default: value of L</File::KDBX/key>)
+* C<mode> - File mode / permissions (see L<perlfunc/"chmod LIST">
+* C<uid> - User ID (see L<perlfunc/"chown LIST">)
+* C<gid> - Group ID (see L<perlfunc/"chown LIST">)
+* C<atomic> - Write to the filepath atomically (default: true)
 
 =cut
 
@@ -167,16 +192,27 @@ sub dump_file {
     my $filepath = shift;
     my %args     = @_ % 2 == 0 ? @_ : (key => shift, @_);
 
-    my $key = delete $args{key};
+    my $key     = delete $args{key};
+    my $mode    = delete $args{mode};
+    my $uid     = delete $args{uid};
+    my $gid     = delete $args{gid};
+    my $atomic  = delete $args{atomic} // 1;
+
     $args{kdbx} //= $self->kdbx;
 
-    require File::Temp;
-    my ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", CLEANUP => 1) };
-    if (!$fh or my $err = $@) {
-        $err //= 'Unknown error';
-        throw sprintf('Open file failed (%s): %s', $filepath_temp, $err),
-            error       => $err,
-            filepath    => $filepath_temp;
+    my ($fh, $filepath_temp);
+    if ($atomic) {
+        require File::Temp;
+        ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", UNLINK => 1) };
+        if (!$fh or my $err = $@) {
+            $err //= 'Unknown error';
+            throw sprintf('Open file failed (%s): %s', $filepath_temp, $err),
+                error       => $err,
+                filepath    => $filepath_temp;
+        }
+    }
+    else {
+        open($fh, '>:raw', $filepath) or throw "Open file failed ($filepath): $!", filepath => $filepath;
     }
     $fh->autoflush(1);
 
@@ -187,22 +223,31 @@ sub dump_file {
 
     my ($file_mode, $file_uid, $file_gid) = (stat($filepath))[2, 4, 5];
 
-    my $mode = $args{mode} // $file_mode // do { my $m = umask; defined $m ? oct(666) &~ $m : undef };
-    my $uid  = $args{uid}  // $file_uid  // -1;
-    my $gid  = $args{gid}  // $file_gid  // -1;
-    chmod($mode, $filepath_temp) if defined $mode;
-    chown($uid, $gid, $filepath_temp);
-    rename($filepath_temp, $filepath) or throw "Failed to write file ($filepath): $!", filepath => $filepath;
+    if ($filepath_temp) {
+        $mode //= $file_mode // do { my $m = umask; defined $m ? oct(666) &~ $m : undef };
+        $uid  //= $file_uid  // -1;
+        $gid  //= $file_gid  // -1;
+        chmod($mode, $filepath_temp) if defined $mode;
+        chown($uid, $gid, $filepath_temp);
+        rename($filepath_temp, $filepath) or throw "Failed to write file ($filepath): $!",
+            filepath => $filepath;
+    }
 
     return $self;
 }
 
 =method dump_handle
 
-    $dumper->dump_handle($fh, $key);
-    $dumper->dump_handle(*IO, $key);
+    $dumper->dump_handle($fh, %options);
+    $dumper->dump_handle(*IO, $key, %options);
+    $dumper->dump_handle($fh, %options);
+    $dumper->dump_handle(*IO, $key, %options);
+
+Dump a KDBX file to an output stream / file handle. Available options:
 
-Dump a KDBX file to an output stream / file handle.
+=for :list
+* C<kdbx> - Database to dump (default: value of L</kdbx>)
+* C<key> - Alternative way to specify C<$key> (default: value of L</File::KDBX/key>)
 
 =cut
 
index 335b31e367fdf91bff45a4a13a69701577fd9826..0b6093f985771182f1d9f048f695223c39e27f99 100644 (file)
@@ -140,6 +140,7 @@ Write a key file. Available options:
 * C<filepath> - Where to save the file (default: value of L</filepath>)
 * C<fh> - IO handle to write to (overrides C<filepath>, one of which must be defined)
 * C<raw_key> - Raw key (default: value of L</raw_key>)
+* C<atomic> - Write to the filepath atomically (default: true)
 
 =cut
 
@@ -156,18 +157,24 @@ sub save {
     my $version     = $args{version} // $self->version // 2;
     my $filepath    = $args{filepath} // $self->filepath;
     my $fh          = $args{fh};
+    my $atomic      = $args{atomic} // 1;
 
     my $filepath_temp;
     if (!openhandle($fh)) {
         $filepath or throw 'Must specify where to safe the key file to';
 
-        require File::Temp;
-        ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", CLEANUP => 1) };
-        if (!$fh or my $err = $@) {
-            $err //= 'Unknown error';
-            throw sprintf('Open file failed (%s): %s', $filepath_temp, $err),
-                error       => $err,
-                filepath    => $filepath_temp;
+        if ($atomic) {
+            require File::Temp;
+            ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", UNLINK => 1) };
+            if (!$fh or my $err = $@) {
+                $err //= 'Unknown error';
+                throw sprintf('Open file failed (%s): %s', $filepath_temp, $err),
+                    error       => $err,
+                    filepath    => $filepath_temp;
+            }
+        }
+        else {
+            open($fh, '>:raw', $filepath) or throw "Open file failed ($filepath): $!", filepath => $filepath;
         }
     }
 
index 628fe98814e8aad3d03c9e59fce792835c09e2c7..3a3c184f95a7e6fa661c4ce11271cc819a472e0c 100644 (file)
@@ -98,14 +98,18 @@ sub reset {
 
 =method load
 
+    $kdbx = File::KDBX::Loader->load(\$string, %options);
     $kdbx = File::KDBX::Loader->load(\$string, $key);
+    $kdbx = File::KDBX::Loader->load(*IO, %options);
     $kdbx = File::KDBX::Loader->load(*IO, $key);
+    $kdbx = File::KDBX::Loader->load($filepath, %options);
     $kdbx = File::KDBX::Loader->load($filepath, $key);
-    $kdbx = $loader->load(...); # also instance method
 
-Load a KDBX file.
+Load a KDBX file. This works as an instance or a class method. The C<$key> is either
+a L<File::KDBX::Key> or a primitive castable to a Key object. Available options:
 
-The C<$key> is either a L<File::KDBX::Key> or a primitive that can be cast to a Key object.
+=for :list
+* C<key> - Alternative way to specify C<$key>
 
 =cut
 
@@ -120,11 +124,15 @@ sub load {
 
 =method load_string
 
+    $kdbx = File::KDBX::Loader->load_string($string, %options);
     $kdbx = File::KDBX::Loader->load_string($string, $key);
+    $kdbx = File::KDBX::Loader->load_string(\$string, %options);
     $kdbx = File::KDBX::Loader->load_string(\$string, $key);
-    $kdbx = $loader->load_string(...); # also instance method
 
-Load a KDBX file from a string / memory buffer.
+Load a KDBX file from a string / memory buffer. This works as an instance or class method. Available options:
+
+=for :list
+* C<key> - Alternative way to specify C<$key>
 
 =cut
 
@@ -147,10 +155,13 @@ sub load_string {
 
 =method load_file
 
+    $kdbx = File::KDBX::Loader->load_file($filepath, %options);
     $kdbx = File::KDBX::Loader->load_file($filepath, $key);
-    $kdbx = $loader->load_file(...); # also instance method
 
-Read a KDBX file from a filesystem.
+Read a KDBX file from a filesystem. This works as an instance or class method. Available options:
+
+=for :list
+* C<key> - Alternative way to specify C<$key>
 
 =cut
 
@@ -171,11 +182,16 @@ sub load_file {
 
 =method load_handle
 
+    $kdbx = File::KDBX::Loader->load_handle($fh, %options);
     $kdbx = File::KDBX::Loader->load_handle($fh, $key);
+    $kdbx = File::KDBX::Loader->load_handle(*IO, %options);
     $kdbx = File::KDBX::Loader->load_handle(*IO, $key);
-    $kdbx->load_handle(...); # also instance method
 
-Read a KDBX file from an input stream / file handle.
+Read a KDBX file from an input stream / file handle. This works as an instance or class method. Available
+options:
+
+=for :list
+* C<key> - Alternative way to specify C<$key>
 
 =cut
 
index 5645b4c4a4fa83e994427335ac850a92730ef8cc..a27f4dd70393a4d9aaa21c68d5f39cb9e32f8517 100644 (file)
@@ -716,8 +716,8 @@ sub query_any {
     $size = read_all($fh, my $buffer, $size);
     $size = read_all($fh, my $buffer, $size, $offset);
 
-Like L<functions/read> but returns C<undef> if not all C<$size> bytes are read. This is considered an error,
-distinguishable from other errors by C<$!> not being set.
+Like L<perlfunc/"read FILEHANDLE,SCALAR,LENGTH,OFFSET"> but returns C<undef> if not all C<$size> bytes are
+read. This is considered an error, distinguishable from other errors by C<$!> not being set.
 
 =cut
 
index d4edfb2662dc81e5624b72d6f44102075f0c3714..8bed335b351ef60e87d4f2d71ae6ff15b7b8de5a 100644 (file)
@@ -9,6 +9,7 @@ use lib "$Bin/lib";
 use TestCommon;
 
 use File::KDBX;
+use File::Temp qw(tempfile);
 use Test::Deep;
 use Test::More;
 use Time::Piece;
@@ -170,4 +171,24 @@ subtest 'Maintenance' => sub {
     is $entry->custom_icon_uuid, $icon_uuid, 'Uses of removed icon change';
 };
 
+subtest 'Dumping to filesystem' => sub {
+    my $kdbx = File::KDBX->new;
+    $kdbx->add_entry(title => 'Foo', password => 'whatever');
+
+    my ($fh, $filepath) = tempfile('kdbx-XXXXXX', TMPDIR => 1, UNLINK => 1);
+    close($fh);
+
+    $kdbx->dump($filepath, 'a');
+
+    my $kdbx2 = File::KDBX->load($filepath, 'a');
+    my $entry = $kdbx2->entries->map(sub { $_->title.'/'.$_->expand_password })->next;
+    is $entry, 'Foo/whatever', 'Dump and load an entry';
+
+    $kdbx->dump($filepath, key => 'a', atomic => 0);
+
+    $kdbx2 = File::KDBX->load($filepath, 'a');
+    $entry = $kdbx2->entries->map(sub { $_->title.'/'.$_->expand_password })->next;
+    is $entry, 'Foo/whatever', 'Dump and load an entry (non-atomic)';
+};
+
 done_testing;
index b42aa235e4d9699bb345da82236952402d711f68..3bf32617e0a15153339f0c9661b7d70677137af5 100644 (file)
@@ -40,9 +40,9 @@ SKIP: {
         $write = File::KDBX::IO::HashBlock->new($write);
         print $write $expected_plaintext;
         close($write) or die "close failed: $!";
-        exit;
-        require POSIX;
-        POSIX::_exit(0);
+        exit;
+        require POSIX;
+        POSIX::_exit(0);
     }
 
     $read = File::KDBX::IO::HashBlock->new($read);
index 87f280913f0b863c1eef8a6ed1fb9441d01b2ea8..035d43319170dc90341d5f31fcf1e88b2d4bae8d 100644 (file)
@@ -44,9 +44,9 @@ SKIP: {
         $write = File::KDBX::IO::HmacBlock->new($write, key => $KEY);
         print $write $expected_plaintext;
         close($write) or die "close failed: $!";
-        exit;
-        require POSIX;
-        POSIX::_exit(0);
+        exit;
+        require POSIX;
+        POSIX::_exit(0);
     }
 
     $read = File::KDBX::IO::HmacBlock->new($read, key => $KEY);
index 65658e5a7391af4fb10ff2ca199ac22e5c2aa036..601260c69de981f7299322ee5263ab44afe5bb29 100644 (file)
--- a/t/keys.t
+++ b/t/keys.t
@@ -55,7 +55,8 @@ for my $test (
     subtest "Save $type key file" => sub {
         my ($type, $filename, $expected_key, $version) = @_;
 
-        my ($fh, $filepath) = tempfile('keyfile-XXXXXX', TMPDIR => 1, UNLINK => 1, SUFFIX => '.key');
+        my ($fh, $filepath) = tempfile('keyfile-XXXXXX', TMPDIR => 1, UNLINK => 1);
+        close($fh);
         note $filepath;
         my $key = File::KDBX::Key::File->new(
             filepath    => $filepath,
@@ -65,7 +66,6 @@ for my $test (
         );
 
         my $e = exception { $key->save };
-        close($fh);
 
         if ($type == KEY_FILE_TYPE_HASHED) {
             like $e, qr/invalid type/i, "Cannot save $type file";
@@ -88,7 +88,7 @@ subtest 'IO handle key files' => sub {
         'Can calculate raw key from file handle' or diag encode_b64($key->raw_key);
     is $key->type, 'hashed', 'file type is detected as hashed';
 
-    my ($fh_save, $filepath) = tempfile('keyfile-XXXXXX', TMPDIR => 1, UNLINK => 1, SUFFIX => '.key');
+    my ($fh_save, $filepath) = tempfile('keyfile-XXXXXX', TMPDIR => 1, UNLINK => 1);
     is exception { $key->save(fh => $fh_save, type => KEY_FILE_TYPE_XML) }, undef,
         'Save key file using IO handle';
     close($fh_save);
This page took 0.033775 seconds and 4 git commands to generate.