File Coverage

File:blib/lib/App/Access2CSV/Exporter.pm
Coverage:98.5%

linestmtbrancondsubtimecode
1package App::Access2CSV::Exporter;
2
3
36
36
36
145521
27
481
use strict;
4
36
36
36
61
22
697
use warnings;
5
36
36
36
443
12095
89
use autodie qw(:all);
6
7
36
36
36
87169
30
949
use Config;
8
9# Sub::Private must be in enforce mode before it is loaded, so that
10# private methods still work through $self->method dispatch
11
36
359
BEGIN { $Sub::Private::config{mode} = 'enforce' }
12
13# Inherit i18n() and the protected _croak_i18n/_carp_i18n helpers
14
36
36
36
66
30
93
use parent 'App::Access2CSV::I18N';
15
16
36
36
36
1104
22
805
use Encode qw(FB_CROAK find_encoding);
17
36
36
36
61
26
812
use File::Path qw(make_path);
18
36
36
36
64
21
367
use File::Spec;
19
36
36
36
7462
137455
1166
use File::Temp;
20
36
35
35
6810
17685
963
use File::Which qw(which);
21
35
34
34
6722
46857
1001
use IPC::Run3 qw(run3);
22
34
34
34
83
24
622
use Params::Get qw(get_params);
23
34
34
34
58
24
439
use Params::Validate::Strict qw(validate_strict);
24
34
34
34
49
24
459
use Readonly;
25
34
34
34
53
30
432
use Return::Set qw(set_return);
26
34
34
34
51
26
437
use Scalar::Util qw(blessed);
27
34
34
34
44
24
78
use Sub::Private;
28
34
34
34
2650
23
73
use Sub::Protected;
29
30our $VERSION = '0.001.0';
31
32# Stop Carp from reporting errors against the access-control wrappers
33our @CARP_NOT = qw(Sub::Private Sub::Protected App::Access2CSV::I18N);
34
35# Exit statuses returned by run()
36Readonly::Scalar my $EXIT_OK      => 0;
37Readonly::Scalar my $EXIT_FAILURE => 1;
38
39# The mdbtools programs; mdb-count is only needed for --show-counts
40Readonly::Scalar my $MDB_TABLES   => 'mdb-tables';
41Readonly::Scalar my $MDB_EXPORT   => 'mdb-export';
42Readonly::Scalar my $MDB_COUNT    => 'mdb-count';
43Readonly::Array  my @REQUIRED_PROGRAMS => ($MDB_TABLES, $MDB_EXPORT);
44
45# Output encodings accepted by --encoding
46Readonly::Scalar my $ENC_UTF8     => 'utf8';
47Readonly::Scalar my $ENC_UTF8_BOM => 'utf8-bom';
48Readonly::Scalar my $ENC_CP1252   => 'cp1252';
49Readonly::Array  my @ENCODINGS    => ($ENC_UTF8, $ENC_UTF8_BOM, $ENC_CP1252);
50
51# Taint mode: a value is untainted only after it has been validated, by
52# capturing it with this pattern (anything non-empty without a NUL byte:
53# a NUL cannot be passed to the operating system at all)
54Readonly::Scalar my $UNTAINT_RE => qr/\A([^\x00]+)\z/s;
55
56# Environment variables that can change how a program is started (see
57# perlsec); removed for the mdbtools processes
58Readonly::Array my @UNSAFE_ENV => qw(IFS CDPATH ENV BASH_ENV);
59
60# Encoding objects, looked up once: calling Encode::decode/encode by name
61# repeats the lookup for every line, which made conversion about 4 times
62# slower on large tables.  (Plain lexicals, not Readonly: Readonly's deep
63# copy could interfere with the objects' internals.)
64my $UTF8_CODEC   = find_encoding('UTF-8');
65my $CP1252_CODEC = find_encoding('cp1252');
66
67# Byte order mark written at the start of utf8-bom files (for Excel)
68Readonly::Scalar my $UTF8_BOM     => "\xEF\xBB\xBF";
69
70# Tables Access creates for itself: MSys*, USys* and ~temporary objects
71Readonly::Scalar my $SYSTEM_TABLE_RE => qr/\A(?:MSys|USys|~)/i;
72
73# Characters that are illegal in a file name on at least one common OS
74Readonly::Scalar my $UNSAFE_CHARS_RE => qr/[<>:"\/\\|?*\x00-\x1F\x7F]/;
75
76# Invisible text-direction controls (LRM, RLM, LRE, RLE, PDF, LRO, RLO,
77# LRI, RLI, FSI, PDI).  They can make a file name display as something
78# else ("report<RLO>vsc.exe.csv"), so they are unsafe like other control
79# characters.  Matched both as Perl characters and as UTF-8 bytes, since
80# table names from mdbtools arrive as bytes.
81Readonly::Scalar my $BIDI_CONTROLS_RE => qr/
82          [\x{200E}\x{200F}\x{202A}-\x{202E}\x{2066}-\x{2069}]   # as characters
83        | \xE2 \x80 [\x8E\x8F\xAA-\xAE]                       # as UTF-8: marks, embeddings, overrides
84        | \xE2 \x81 [\xA6-\xA9]                              # as UTF-8: isolates
85/x;
86
87# Device names Windows reserves whatever the extension (CON.csv is illegal)
88Readonly::Scalar my $RESERVED_NAME_RE => qr/\A(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])\z/i;
89
90# Name used when sanitising leaves nothing, and the extension we write
91Readonly::Scalar my $UNNAMED      => 'unnamed';
92Readonly::Scalar my $CSV_SUFFIX   => '.csv';
93
94# Ends option parsing in mdbtools, so a table or database name that
95# starts with "-" is never taken for an option (glib parses options
96# anywhere on the command line, not only before the file name)
97Readonly::Scalar my $END_OF_OPTIONS => '--';
98
99# Temporary files are hidden and live next to the target for atomic rename
100Readonly::Scalar my $TEMP_TEMPLATE => '.access2csv-XXXXXX';
101
102# How much of an unreadable mdb-count answer to quote in the warning
103Readonly::Scalar my $COUNT_SHOWN => 40;
104
105# Shown in the dry run's ROWS column when a table could not be counted
106Readonly::Scalar my $UNKNOWN_COUNT => '?';
107
108# Dry-run table layout
109Readonly::Scalar my $TABLE_COLUMN_WIDTH => 40;
110Readonly::Scalar my $ROWS_COLUMN_WIDTH  => 10;
111Readonly::Scalar my $RULE_WIDTH         => 70;
112
113# Return schema of run(), shared by its two exits
114Readonly::Hash my %RUN_STATUS_SCHEMA => (type => 'integer', min => $EXIT_OK, max => $EXIT_FAILURE);
115
116# Mode bits for new files before the umask is applied
117Readonly::Scalar my $FILE_MODE    => oct('666');
118
119# Default settings; the flat scalar layout is compatible with Object::Configure
120Readonly::Hash my %DEFAULTS => (
121        output_dir  => File::Spec->curdir(),
122        overwrite   => 0,
123        verbose     => 0,
124        dry_run     => 0,
125        show_counts => 0,
126        progress    => 1,
127        encoding    => $ENC_UTF8,
128);
129
130# Constructor argument schema, shared by new() and the POD
131Readonly::Hash my %NEW_SCHEMA => (
132        output_dir  => { type => 'string', min => 1, optional => 1 },
133        tables      => { type => 'arrayref', element_type => 'string', optional => 1 },
134        overwrite   => { type => 'boolean', optional => 1 },
135        verbose     => { type => 'boolean', optional => 1 },
136        dry_run     => { type => 'boolean', optional => 1 },
137        show_counts => { type => 'boolean', optional => 1 },
138        progress    => { type => 'boolean', optional => 1 },
139        encoding    => { type => 'string', memberof => [@ENCODINGS], optional => 1 },
140        logger      => { type => 'object', can => ['debug', 'info', 'warn'], optional => 1 },
141        language    => { type => 'string', optional => 1 },
142);
143
144=encoding utf8
145
146 - 432
=head1 NAME

App::Access2CSV::Exporter - Export the tables of a Microsoft Access database to CSV files

=head1 VERSION

Version 0.001.0

=head1 SYNOPSIS

        use App::Access2CSV::Exporter;

        # 1. The simplest case: every table, into the current folder
        my $exporter = App::Access2CSV::Exporter->new();
        my $status = $exporter->run('shop.accdb');   # 0 = all OK, 1 = some failed

        # 2. Some tables, into a folder, for Excel, replacing old files
        my $exporter = App::Access2CSV::Exporter->new(
                output_dir => 'exports',
                tables     => ['Customers', 'Orders'],
                encoding   => 'utf8-bom',
                overwrite  => 1,
        );
        $exporter->run('shop.accdb');

        # 3. Only look: print the table list and row counts, write nothing
        App::Access2CSV::Exporter->new(dry_run => 1, show_counts => 1)->run('shop.accdb');

        # 4. Inside a larger program: no progress lines, a log, and full
        #    error handling
        use Log::Abstraction;

        my $exporter = App::Access2CSV::Exporter->new({
                output_dir => '/srv/exports',
                progress   => 0,
                logger     => Log::Abstraction->new(logger => '/var/log/export.log'),
        });
        my $status = eval { $exporter->run('/data/shop.accdb') };
        if(!defined $status) {
                die "Nothing was exported: $@";       # for example, the file is missing
        } elsif($status == 1) {
                warn "Some tables were not exported; see the log\n";
        }

=head1 DESCRIPTION

This module does the real work of the C<access2csv> program.  It writes
one CSV file for each table of a Microsoft Access database.

It runs three programs from the B<mdbtools> package: C<mdb-tables> (to
list the tables), C<mdb-export> (to get each table as CSV) and, only when
row counts are wanted, C<mdb-count>.  They must be in your C<PATH>.

Access's own internal tables (names starting with C<MSys>, C<USys> or
C<~>) are skipped.

Each file is first written to a hidden temporary file in the output
folder, and renamed to its real name only when it is complete.  So a
failed export never leaves a half-written CSV file, and an old file is
only replaced by a complete new one.  New files get the usual
permissions (0666 minus your umask).

An exporter can be used for more than one C<run>.  Each C<run> starts
again with the same file names, so running twice gives the same files.

The mdbtools programs are looked up only in absolute C<PATH> folders, so
a program planted in the current folder is never run, and they are
started with a cleaned environment (see L<App::Access2CSV/SECURITY>).
The module works under taint mode (C<perl -T>).  Table names are
printed and logged with control characters escaped, so a hostile name
cannot send escape sequences to your terminal.

Table names and the database path are handed to mdbtools as separate
arguments, never through a shell, and after a C<--> marker.  So names
containing shell characters (C<; | E<gt> $( )>), spaces or newlines, or
starting with C<->, are always treated as names, never as commands or
options.  A table name can never place a file outside the output
folder: C</> and C<\> are replaced, and names cannot start with a dot.

An existing entry at the target name - including a symbolic link, even a
broken one - counts as "already exists".  With C<overwrite>, the link
itself is replaced; the file it pointed to is never written.

The rules for file names are described in
L<App::Access2CSV/How the CSV files are named>.

=head1 ENCODING

=over 4

=item * B<CSV data.>  mdbtools gives UTF-8.  With C<encoding> set to
C<utf8> or C<utf8-bom> the bytes are copied exactly, so every character,
including emoji and non-Latin scripts, is kept.  C<utf8-bom> also writes
the three-byte UTF-8 "byte order mark" first, which Microsoft Excel
needs.  With C<cp1252>, each line is converted to Windows-1252; if a line
has a character that Windows-1252 does not have (for example Greek,
Chinese or an emoji), that table fails and nothing is written for it.

=item * B<Database path and output_dir.>  These are passed to the operating
system unchanged.  Give them as byte strings (the form you get from
C<@ARGV> or C<readdir>).  Non-ASCII names work on systems whose file names
are UTF-8, such as Linux and macOS.

=item * B<Table names> (in C<tables>).  They are compared with the names
that C<mdb-tables> prints, which are UTF-8 bytes.  So give UTF-8 byte
strings, not decoded Perl character strings.  If you have a decoded
string, use C<Encode::encode('UTF-8', $name)> first.  The CSV file name is
made from the same bytes, so non-ASCII names and emoji are kept.

=item * B<Messages.>  All messages are plain ASCII English.

=back

=head1 COMMON PITFALLS

=over 4

=item * B<undef means "use the default", not "false".>  In C<new>,
C<< overwrite => undef >> is the same as not giving C<overwrite> at all.
To switch something off, give C<0>.

=item * B<An empty table list exports nothing.>  C<< tables => undef >>
(or no C<tables>) means "all tables".  C<< tables => [] >> means "no
tables": nothing is exported, and C<run> returns 0.

=item * B<Table names are case-sensitive.>  C<'orders'> does not match the
table C<Orders>.  Names that do not match any table give a warning.

=item * B<A failing logger does not stop the export.>  If the logger dies
(for example, its disk is full), C<run> warns once with "Cannot write to
the log", stops logging for this exporter, and carries on exporting.

=item * B<run can croak.>  C<run> returns 1 when some tables fail, but it
croaks (throws an exception) when nothing can be exported at all: the
database is missing or unreadable, mdbtools is not installed, or the
output folder cannot be created.  Wrap C<run> in C<eval> if your program
must keep going.

=item * B<run may change the show_counts setting.>  If C<show_counts> is
on but C<mdb-count> cannot be found, C<run> warns and switches
C<show_counts> off for this exporter.

=item * B<Settings are copied, not shared.>  C<new> makes its own copy of
the C<tables> list; changing your array later has no effect.  Settings are
not merged in depth: a new C<tables> list replaces the default completely.

=item * B<Warnings go through carp.>  Failed tables and unknown table names
are reported with C<carp>, so they appear on standard error (or in your
C<$SIG{__WARN__}> handler) even when a logger is given.

=item * B<Load the module with use, not require.>  Protection of the
private methods is set up at compile time.  After a run-time
C<require> Perl prints "Too late to run CHECK block" and the protection is
missing.

=back

=head1 METHODS

=head2 new

=head3 Purpose

Make a new exporter with your settings.  Nothing is checked on disk yet.

=head3 Arguments

Named arguments, either as a list or as one hash reference.  All of them
are optional.  An argument whose value is C<undef> is ignored, so its
default is used.

=over 4

=item C<output_dir> - the folder for the CSV files.  Default: the current folder.

=item C<tables> - an array reference of table names to export.  Default:
all tables.  An empty array means no tables.

=item C<overwrite> - true to replace CSV files that already exist.  Default: false.

=item C<verbose> - true to log extra detail.  Default: false.

=item C<dry_run> - true to only print what would be written.  Default: false.

=item C<show_counts> - true to report row counts (needs C<mdb-count>).  Default: false.

=item C<progress> - true to print C<[n/total] table> lines to standard
error.  Default: true.

=item C<encoding> - C<utf8>, C<utf8-bom> or C<cp1252>.  Default: C<utf8>.

=item C<logger> - an object with C<debug>, C<info> and C<warn> methods,
such as a L<Log::Abstraction> object.  Default: no logging.

=item C<language> - a language code such as C<en> for messages.  Default:
taken from the locale (see L<App::Access2CSV::I18N>).

=back

=head3 Returns

A new C<App::Access2CSV::Exporter> object.

=head3 Side Effects

None.  Your C<$@>, C<$!> and C<$_> are left as they were.

=head3 Usage

        my $exporter = App::Access2CSV::Exporter->new({ dry_run => 1 });

=head3 EXAMPLE

        # Export two tables as Windows-1252, replacing older files, with a log
        my $exporter = App::Access2CSV::Exporter->new(
                output_dir => 'out',
                tables     => ['Customers', 'Orders'],
                encoding   => 'cp1252',
                overwrite  => 1,
                logger     => Log::Abstraction->new(logger => 'export.log'),
        );

=head3 API SPECIFICATION

=head4 Input

        {
                output_dir  => { type => 'string', min => 1, optional => 1 },
                tables      => { type => 'arrayref', element_type => 'string', optional => 1 },
                overwrite   => { type => 'boolean', optional => 1 },
                verbose     => { type => 'boolean', optional => 1 },
                dry_run     => { type => 'boolean', optional => 1 },
                show_counts => { type => 'boolean', optional => 1 },
                progress    => { type => 'boolean', optional => 1 },
                encoding    => { type => 'string', memberof => ['utf8', 'utf8-bom', 'cp1252'], optional => 1 },
                logger      => { type => 'object', can => ['debug', 'info', 'warn'], optional => 1 },
                language    => { type => 'string', optional => 1 },
        }

Valid and invalid values (tested in F<t/domain.t>):

        output_dir  valid:   any path of 1 character or more ("0" is valid),
                             including non-ASCII names given as UTF-8 bytes
                    invalid: "" (below the minimum), references
        tables      valid:   undef (all tables), [] (no tables), one name,
                             many names; names are matched byte for byte
                    invalid: anything but an array reference; elements
                             that are references
        booleans    valid:   exactly 1 true TRUE yes on  /  0 false FALSE no off
        (overwrite, invalid: everything else, e.g. "", 2, -1, "True", "Yes",
         verbose,            "0.0", " 1"
         dry_run, show_counts, progress)
        encoding    valid:   exactly utf8, utf8-bom, cp1252
                    invalid: other spellings (UTF8, utf-8, CP1252, " utf8")
        logger      valid:   an object with debug, info and warn methods
                    invalid: an object missing any of them, a plain hash,
                             a string
        language    valid:   any string; "" means "use the environment"
                    invalid: references

=head4 Output

        {
                type => 'object',
                isa  => 'App::Access2CSV::Exporter',
        }

=head3 MESSAGES

All are fatal, and read "Invalid setting: REASON".  The REASON part
comes from L<Params::Validate::Strict> and is not translated; for example:

        +--------------------------------------+------------------------------+-----------------------------+
        | Message                              | Meaning                      | What to do                  |
        +--------------------------------------+------------------------------+-----------------------------+
        | Invalid setting: Unknown parameter   | X is not a known setting     | Remove X, or fix its        |
        |  'X'                                 |                              | spelling                    |
        | Invalid setting: Parameter           | This encoding is not         | Use utf8, utf8-bom or       |
        |  'encoding' (X) must be one of utf8, | supported                    | cp1252                      |
        |  utf8-bom, cp1252                    |                              |                             |
        | Invalid setting: Parameter 'logger'  | logger is not an object      | Give a logger object        |
        |  must be an object                   |                              |                             |
        | Invalid setting: Parameter 'tables'  | tables is not an array       | Give an array reference     |
        |  must be ...                         | reference                    |                             |
        +--------------------------------------+------------------------------+-----------------------------+

=cut
433
434sub new {
435
1785
9494299
        my $class = shift;
436
437        # Validation uses eval internally; the caller's $@ must survive
438
1785
1392
        local $@;
439
440        # Drop undefined values so that "not given" means "use the default"
441
1785
3598
        my $args = get_params(undef, \@_) || {};
442
1784
2733
2975
1784
22264
2768
2557
1960
        my %given = map { $_ => $args->{$_} } grep { defined $args->{$_} } keys %{$args};
443
444        # A bad setting is reported in plain words ("Invalid setting: ..."),
445        # not with Params::Validate::Strict's internal prefix and location
446
1784
1784
1655
3032
        my $params = eval { validate_strict(schema => { %NEW_SCHEMA }, input => \%given) }
447                or $class->_croak_i18n('invalid_setting', { params => [_validation_reason($@)] });
448
449        # Copy the table list so later changes by the caller cannot affect us
450
1692
46
705974
208
        $params->{tables} = [ @{ $params->{tables} } ] if $params->{tables};
451
452        # The output folder is the invoking user's own choice; it is only used
453        # as a folder name, so it is untainted here (see _untaint)
454
1692
2304
        $params->{output_dir} = _untaint($params->{output_dir}) if defined $params->{output_dir};
455
456
1692
1692
2279
20126
        my $self = bless { %DEFAULTS, %{$params}, used_names => {}, next_suffix => {}, programs => {} }, $class;
457
1692
25948
        return set_return($self, { type => 'object' });
458}
459
460 - 671
=head2 run

=head3 Purpose

Export the selected tables of one database to CSV files.  In dry-run mode,
only print what would be exported.

=head3 Arguments

=over 4

=item C<database> (string, required) - the path of the C<.mdb> or C<.accdb> file.
The name is taken literally: C<-> is a file called C<->.  (Reading from
standard input is a feature of the command-line program; see
L<App::Access2CSV/Reading the database from standard input>.)

=back

You can give it on its own, C<< $exporter->run('shop.accdb') >>, or as a
hash reference, C<< $exporter->run({ database => 'shop.accdb' }) >>.

=head3 Returns

C<0> if every selected table was exported, or in dry-run mode.
C<1> if at least one table was not exported (the others were).

=head3 Side Effects

=over 4

=item * Creates the output folder if needed (not in dry-run mode, and
not when no table is selected).

=item * Writes one CSV file per table (not in dry-run mode).

=item * Prints progress lines to standard error, if C<progress> is on.

=item * Prints the dry-run list to standard output, in dry-run mode.

=item * Sends messages to the logger, if there is one.

=item * Warns (with C<carp>) about each table that failed, about unknown
names in C<tables>, and about a missing C<mdb-count>.

=item * Croaks, before writing anything, if the database cannot be read, a
needed mdbtools program is missing, C<mdb-tables> fails, or the output
folder cannot be created.

=item * Switches C<show_counts> off for this exporter if C<mdb-count> is
missing.

=item * While it runs, handles the signals INT, QUIT, TERM and HUP (only
those you have not set a handler for yourself; they are restored when
C<run> returns).  Any of them stops the run: the table being exported is
discarded - its temporary file deleted, any old CSV file left as it was -
no further table is started, and C<run> croaks.  Pressing Ctrl-C, which
also stops the mdbtools program, has the same effect.

=item * Leaves your C<$@>, C<$!>, C<$?>, C<$_>, C<$.> and any pending C<alarm>
as they were (except that a croak sets C<$@> in your C<eval>, as usual).

=back

=head3 Usage

        exit $exporter->run('shop.accdb');

=head3 EXAMPLE

        my $exporter = App::Access2CSV::Exporter->new(output_dir => 'out');

        # eval catches the fatal errors; the return value covers the rest
        my $status = eval { $exporter->run('shop.accdb') };
        if(!defined $status) {
                print STDERR "Nothing was exported: $@";
        } elsif($status) {
                print STDERR "Some tables failed; see the warnings above\n";
        } else {
                print "Done\n";
        }

=head3 API SPECIFICATION

=head4 Input

        {
                database => {
                        type     => 'string',
                        min      => 1,
                        optional => 0,
                },
        }

Valid and invalid values (tested in F<t/domain.t>):

        database    valid:   a readable regular file
                    invalid: "" (below the 1-character minimum), undef,
                             a missing file, a folder, a device or FIFO,
                             an unreadable file
                    edges:   each part of the path may be up to 255 bytes;
                             256 gives "File name too long"

The table names that mdbtools reports are data, not arguments, but they
have limits of their own:

        length      a CSV file name is the table name plus ".csv", and the
                    file system limits file names, so table names up to 251
                    units work and longer ones fail (that table only).  The
                    unit depends on the file system: Linux counts bytes (125
                    u-umlauts, 2 bytes each, fit; 126 do not), macOS counts
                    characters (up to 251 of any letter fit).  Access allows
                    at most 64 characters, well within either limit.
        characters  non-ASCII letters, emoji, joined emoji, combining marks
                    and right-to-left text are kept byte for byte.
                    Characters that are unsafe in file names - including
                    invisible text-direction controls such as U+202E - are
                    replaced by "_".
        collisions  the first name has no suffix, then _2, _3, ... _10 ...
        cp1252      U+00FF and the Euro sign convert; U+0100 and above
                    (except the few Windows-1252 symbols), the C1 controls
                    U+0080-U+009F and emoji make the table fail.

=head4 Output

        {
                type => 'integer',
                min  => 0,
                max  => 1,
        }

=head3 MESSAGES

"fatal" means C<run> croaks and nothing is exported.  "per table" means
only that table fails; C<run> warns, logs, and carries on.

        +-----------------------------------------+------------------------------+-------------------------------+
        | Message                                 | Meaning                      | What to do                    |
        +-----------------------------------------+------------------------------+-------------------------------+
        | Interrupted by SIGx: stopped, and the   | Ctrl-C, Ctrl-\\, kill or a   | Run again; tables finished    |
        |  table being exported was discarded     | closed terminal stopped the  | before the interruption are   |
        |  (fatal)                                | run                          | complete                      |
        | run() must be called on an object       | run was called on the class  | Call new() first, then run()  |
        |  created by new() (fatal)               | or on something that is not  | on the object it returns      |
        |                                         | an exporter                  |                               |
        | Cannot read database F: E (fatal)       | F does not exist, or cannot  | Check the path                |
        |                                         | be reached; E is the reason  |                               |
        |                                         | from the operating system    |                               |
        | Database F is not a regular file (fatal)| F is a folder or a device    | Give the database file        |
        | Database F is not readable (fatal)      | No permission to read F      | Fix the permissions           |
        |                                         | (never happens for root)     |                               |
        | Required program not found in PATH: P   | mdbtools is not installed,   | Install mdbtools, or fix PATH |
        |  (fatal)                                | or not in PATH               |                               |
        | mdb-tables failed with exit status N: E | mdbtools cannot read the     | Check that F is a real Access |
        |  (fatal)                                | file                         | database                      |
        | Cannot create output directory D: E     | The folder cannot be made;   | Check permissions and path    |
        |  (fatal)                                | E is the reason for D itself |                               |
        |                                         | (e.g. "Not a directory" when |                               |
        |                                         | a file is in the way)        |                               |
        | Cannot count the rows of T: E (warning) | mdb-count failed for table T | The table is still exported   |
        |                                         | (only with show_counts)      | (dry run: count shown as "?") |
        |  ... mdb-count printed no number: "X"   | mdb-count's answer was not   | As above; X shows what it     |
        |                                         | just a number                | printed                       |
        | Tables not found in database: T         | Names in tables are not in   | Check spelling and case       |
        |  (warning)                              | the database                 |                               |
        | mdb-count not found in PATH; row counts | show_counts is on, but       | Install mdb-count, or turn    |
        |  are unavailable (warning)              | mdb-count is missing         | show_counts off               |
        | FAILED: T: E (warning, logged)          | Table T was not exported,    | See E, one of the messages    |
        |                                         | because of E                 | below                         |
        | Output file already exists: F (use      | F exists (a symbolic link,   | Set overwrite, or use another |
        |  --overwrite to replace it) (per table) | even a broken one, counts)   | output_dir                    |
        |                                         | and overwrite is off         |                               |
        | mdb-export failed with exit status N: E | mdbtools could not read this | Check the table in Access     |
        |  (per table)                            | table                        |                               |
        | P was killed by signal N (per table,    | The program was stopped from | Check memory and system       |
        |  or fatal for mdb-tables)               | outside                      | limits                        |
        | P could not be run: E (per table, or    | The program was found but    | Check its permissions and     |
        |  fatal for mdb-tables)                  | could not be started         | that it is a real program     |
        | Table T, line N: cannot be represented  | A character is not in        | Use utf8 or utf8-bom          |
        |  in cp1252 (per table)                  | Windows-1252                 |                               |
        | Table T, line N: output of mdb-export   | mdbtools gave bytes that are | Check the MDB_ICONV setting   |
        |  is not valid UTF-8 (per table)         | not UTF-8                    |                               |
        | Cannot write F: E (per table)           | The file could not be        | Check permissions and free    |
        |                                         | written or renamed into place| disk space                    |
        | Cannot write to the log: E (warning,    | The logger failed.  Exports  | Check the log's disk or       |
        |  once)                                  | go on; logging stops         | destination                   |
        +-----------------------------------------+------------------------------+-------------------------------+

=head3 PSEUDOCODE

        check the argument
        stop (croak) unless the database is a readable file
        find mdb-tables and mdb-export (croak if missing),
             and mdb-count if row counts are wanted (warn if missing)
        forget the file names given out by any earlier run
        tables := the sorted user tables, filtered by "tables"
                  (warn about names that are not found)
        if dry run:
                print the table -> file list (row count "?" with a warning
                      if a count fails)
                return 0
        if there are tables to export:
                create the output folder (croak if that fails)
        for each table:
                print "[n/total] table" if progress is on
                try to export the table
                if that failed: warn, log, and count the failure
                (a failed row count after the file is in place is only a
                 warning; the table still counts as exported)
        log the summary
        return 1 if any table failed, else 0

=cut
672
673sub run {
674
388
109553
        my $self = shift;
675
676        # State machine guard: run() is only a transition out of READY, which
677        # only new() can create.  Refuse anything else before doing any work.
678        # (Reported through the class: $self may not even be an object.)
679
388
2284
        blessed($self) && $self->isa(__PACKAGE__) or __PACKAGE__->_croak_i18n('needs_object');
680
681        # File tests, evals and child processes below would otherwise leave
682        # their marks in the caller's $@ and $!
683
384
2957
        local ($@, $!);
684
685        # An undef database is a missing one, not a file called "".  Work on a
686        # copy: get_params hands back the caller's own hash when given one.
687        # (Params::Get either dies or returns a hash reference - proved in
688        # t/path.t - so no test of what it returned is needed.)
689
384
384
370
718
        my $input = { %{ get_params('database', \@_) } };
690
378
6258
        delete $input->{database} unless defined $input->{database};
691
378
1012
        my $params = validate_strict(
692                schema => { database => { type => 'string', min => 1 } },
693                input  => $input,
694        );
695
369
22549
        my $database = $params->{database};
696
697        # Fail fast, before any output, on problems that affect every table
698
369
972
        $self->_check_database($database)
699                ->_verify_dependencies()
700                ->_reset_names();
701
702        # Stopping part-way must behave like a failed transaction: the table
703        # being exported is discarded (its temporary file deleted) and no
704        # further table is started.  Perl's default action for these signals
705        # is to exit at once, skipping the clean-up, so while run() is active
706        # they raise an exception instead.  A handler the caller has set is
707        # left alone; everything is restored when run() returns.
708
299
486
        local $self->{interrupted};
709
299
299
258
768
        my @ours = @{ $self->_interrupt_signals() };
710        local @SIG{@ours} = (sub {
711
1
19978
                $self->{interrupted} = $_[0];
712
1
18
                die $self->_printable($self->i18n('interrupted', { params => [$_[0]] })), "\n";
713
299
5804
        }) x @ours;
714
715        # Premise: the database is now known to be a readable regular file, and
716        # it is only ever passed to mdbtools as one list argument after "--".
717        # Conclusion: it is safe to untaint.
718
299
472
        $database = _untaint($database);
719
720
299
744
        my $tables = $self->_select_tables($self->_get_tables($database));
721
722        # Guard clause: a dry run must not touch the file system, so it leaves
723        # before mkdir.  Premise: _dry_run writes nothing that can fail a table.
724        # Conclusion: a dry run always succeeds.
725
283
860
        if($self->{dry_run}) {
726
63
371
                $self->_dry_run($database, $tables);
727
63
438
                return set_return($EXIT_OK, { %RUN_STATUS_SCHEMA });
728        }
729
730        # The output folder is only made when there is something to put in it;
731        # with no tables selected the run goes straight to the summary
732
220
220
339
1021
        my $failed = (@{$tables} ? $self->_make_output_dir() : $self)->_export_all($database, $tables);
733
211
1910
        return set_return($failed ? $EXIT_FAILURE : $EXIT_OK, { %RUN_STATUS_SCHEMA });
734}
735
736# _check_database
737# Purpose:        Make sure the database is a readable regular file.
738# Entry Criteria: $database is a defined, non-empty path.
739# Exit Status:    Returns $self for chaining; croaks otherwise.
740# Side Effects:   stat()s the file; sets $!.
741sub _check_database :Private {
742
373
6820
        my ($self, $database) = @_;
743
744        # The stat result is reused via "_" so the file is only examined once;
745        # $! is captured straight away because later calls may overwrite it
746
373
2269
        if(!-e $database) {
747
36
396
                $self->_croak_i18n('database_not_found', { params => [$database, "$!"] });
748        }
749
337
518
        $self->_croak_i18n('database_not_file', { params => [$database] }) unless -f _;
750
324
731
        $self->_croak_i18n('database_unreadable', { params => [$database] }) unless -r _;
751
752
318
781
        return $self;
753
34
34
34
23394
28
541
}
754
755# _verify_dependencies
756# Purpose:        Locate the mdbtools programs in PATH.
757# Entry Criteria: None.
758# Exit Status:    Returns $self; croaks if a required program is missing.
759# Side Effects:   Sets $self->{programs}; may switch off show_counts (with a
760#                 warning) when mdb-count is unavailable; logs at debug level.
761sub _verify_dependencies :Private {
762
341
5399
        my $self = shift;
763
764
341
348
        my %programs;
765
341
1102
        foreach my $program (@REQUIRED_PROGRAMS) {
766
658
2382
                $programs{$program} = $self->_find_program($program)
767                        or $self->_croak_i18n('program_missing', { params => [$program] });
768        }
769
770        # mdb-count is only needed for row counts, so its absence is not fatal.
771        # _find_program returns a path or false, so one branch decides both
772        # "store it" and "switch counts off" (nothing is stored and removed).
773
302
1021
        if($self->{show_counts}) {
774
47
126
                if(my $path = $self->_find_program($MDB_COUNT)) {
775
35
87
                        $programs{$MDB_COUNT} = $path;
776                } else {
777
12
31
                        $self->{show_counts} = 0;
778
12
45
                        $self->_warn('no_row_counter');
779                }
780        }
781
782
302
487
        $self->{programs} = \%programs;
783
302
785
        return $self;
784
34
34
34
6453
28
273
}
785
786# _find_program
787# Purpose:        Look up one program in PATH and note where it was found.
788# Entry Criteria: $program is a bare program name.
789# Exit Status:    Returns the full path, or undef if not found.
790# Side Effects:   Logs the location at debug level when --verbose is on.
791sub _find_program :Private {
792
704
4282
        my ($self, $program) = @_;
793
794        # Only absolute paths are trusted.  A relative entry in PATH (".", or
795        # an empty one) would run whatever file of that name is in the current
796        # folder - a classic way to plant a program.
797
704
689
3235
59029
        my ($path) = grep { defined && File::Spec->file_name_is_absolute($_) } which($program);
798
799        # An absolute path to an existing program: safe to untaint
800
704
1700
        $path = _untaint($path) if defined $path;
801
704
2129
        if($path && $self->{verbose}) {
802
8
24
                $self->_log(debug => 'program_found', { params => [$program, $path] });
803        }
804
704
1847
        return $path;
805
34
34
34
5219
32
305
}
806
807# _reset_names
808# Purpose:        Forget file names allocated by a previous run() so that
809#                 running the same exporter twice gives the same names.
810# Entry Criteria: None.
811# Exit Status:    Returns $self.
812# Side Effects:   Empties $self->{used_names}.
813sub _reset_names :Private {
814
299
1880
        my $self = shift;
815
816
299
365
        $self->{used_names} = {};
817
299
387
        $self->{next_suffix} = {};
818
299
282
        return $self;
819
34
34
34
3666
41
264
}
820
821# _get_tables
822# Purpose:        List the user tables in the database.
823# Entry Criteria: _verify_dependencies() has run.
824# Exit Status:    Returns an arrayref of table names, sorted; croaks if
825#                 mdb-tables fails.
826# Side Effects:   Runs mdb-tables.
827sub _get_tables :Protected {
828
295
1950
        my ($self, $database) = @_;
829
830        # -1 puts one table per line, so names containing spaces survive;
831        # "--" stops a database path starting with "-" being read as an option
832
295
343
        my $stdout = '';
833
295
1074
        $self->_run_program($MDB_TABLES, ['-1', $END_OF_OPTIONS, $database], \$stdout);
834
835        # \r? copes with mdbtools builds that emit CRLF line endings; a program
836        # that printed nothing may leave $stdout undefined
837        # Table names are untainted: they are only used as one list argument
838        # after "--", and in file names only after _csv_filename has made them
839        # safe
840
279
1502
401519
128650
2307
341892
        my @tables = sort map { _untaint($_) } grep { length($_) && !$self->_is_system_table($_) } split /\r?\n/, $stdout // '';
841
279
13294
        return \@tables;
842
34
34
34
5181
62
309
}
843
844# _is_system_table
845# Purpose:        Decide whether a table is Access's own rather than the user's.
846# Entry Criteria: $table is a table name.
847# Exit Status:    Returns 1 for system tables, 0 otherwise.
848# Side Effects:   None.  Protected so that subclasses can widen the filter.
849sub _is_system_table :Protected {
850
401528
1266064
        my ($self, $table) = @_;
851
852
401528
575533
        return ($table =~ $SYSTEM_TABLE_RE) ? 1 : 0;
853
34
34
34
3697
46
294
}
854
855# _select_tables
856# Purpose:        Apply the --table filter to the list of tables.
857# Entry Criteria: $tables is the arrayref from _get_tables().
858# Exit Status:    Returns an arrayref, in database (sorted) order.
859# Side Effects:   Warns and logs about requested tables that do not exist.
860sub _select_tables :Private {
861
289
3248
        my ($self, $tables) = @_;
862
863
289
1164
        return $tables unless $self->{tables};
864
865        # Matching is exact, as mdb-export itself is case-sensitive
866
35
87
35
104
287
77
        my %available = map { $_ => 1 } @{$tables};
867
35
1050
35
67
995
93
        my %wanted    = map { $_ => 1 } @{ $self->{tables} };
868
869
35
51
132
106
        my @missing = sort grep { !$available{$_} } keys %wanted;
870
35
173
        if(@missing) {
871
12
184
                $self->_warn('unknown_tables', { params => [join(', ', @missing)], count => scalar(@missing) });
872        }
873
874
35
87
35
68
177
74
        return [ grep { $wanted{$_} } @{$tables} ];
875
34
34
34
5804
34
266
}
876
877# _make_output_dir
878# Purpose:        Create the output directory (and parents) if necessary.
879# Entry Criteria: Not in dry-run mode.
880# Exit Status:    Returns $self; croaks if the directory cannot be created.
881# Side Effects:   Creates directories.
882sub _make_output_dir :Private {
883
218
2900
        my $self = shift;
884
885
218
515
        my $dir = $self->{output_dir};
886
218
2566
        return $self if -d $dir;
887
888        # Ask File::Path to report errors rather than carp/croak on its own,
889        # so the message can be translated and names the directory we wanted
890
164
24418
        make_path($dir, { error => \my $errors });
891
164
164
462
1351
        if(@{$errors} || !-d $dir) {
892                # File::Path may also report a parent (e.g. "File exists" for a
893                # plain file in the way); the reason for $dir itself, or failing
894                # that the last one, is what explains the failure
895
18
24
18
32
40
27
                my ($mine) = grep { exists $_->{$dir} } @{$errors};
896
18
6
2
46
9
2
                my $detail = $mine ? $mine->{$dir} : (@{$errors} ? (values %{ $errors->[-1] })[0] : undef);
897
18
135
                $self->_croak_i18n('mkdir_failed', { params => [$dir, $detail || "$!"] });
898        }
899
146
606
        return $self;
900
34
34
34
5745
26
317
}
901
902# _export_all
903# Purpose:        Export each table, carrying on past individual failures.
904# Entry Criteria: The output directory exists.
905# Exit Status:    Returns the number of tables that failed.
906# Side Effects:   Writes CSV files; prints progress; warns; logs.
907sub _export_all :Private {
908
216
3002
        my ($self, $database, $tables) = @_;
909
910
216
216
238
300
        my $total  = scalar @{$tables};
911
216
317
        my $failed = 0;
912
913        # eval below would otherwise overwrite the caller's $@
914
216
261
        local $@;
915
916        # An index loop, not each(), which shares the array's iterator with the
917        # caller and would silently skip tables if it was already part-way
918
216
216
294
445
        foreach my $index (0 .. $#{$tables}) {
919
1369
1823
                my $table = $tables->[$index];
920                # Progress goes to STDERR so that STDOUT can be redirected cleanly
921
1369
1974
                if($self->{progress}) {
922
53
471
                        print STDERR $self->_printable($self->i18n('progress', { params => [$index + 1, $total, $table] })), "\n";
923                }
924
925                # One bad table should not stop the rest from being exported
926
1369
1369
1290
1165
2632
35327
                next if eval { $self->_export_table($database, $table); 1 };
927
928                # ... but an interruption stops them all: the failed table has been
929                # discarded (its temporary file went with the exception), and no
930                # further table is started
931
79
30152
                $self->_croak_i18n('interrupted', { params => [$self->{interrupted}] }) if $self->{interrupted};
932
933
77
251
                my $error = $@ || 'Unknown error';
934
77
119
                chomp $error;
935
77
96
                ++$failed;
936
77
329
                $self->_warn('export_failed', { params => [$table, $error] });
937        }
938
939
214
1047
        $self->_log(info => 'summary', { params => [$total, $failed], count => $total });
940
214
811
        return $failed;
941
34
34
34
6735
43
270
}
942
943# _export_table
944# Purpose:        Export one table to its CSV file.
945# Entry Criteria: _verify_dependencies() and _make_output_dir() have run.
946# Exit Status:    Returns $self; croaks on any failure, leaving no partial file.
947# Side Effects:   Runs mdb-export (and mdb-count); creates or replaces a file.
948sub _export_table :Protected {
949
1369
9465
        my ($self, $database, $table) = @_;
950
951
1369
2565
        my $outfile = File::Spec->catfile($self->{output_dir}, $self->_csv_filename($table));
952
953        # Check before exporting so that we do not waste time on a big table
954        # -l as well as -e: a dangling symlink is an existing entry too, and
955        # must not be silently replaced.  The overwrite flag is tested first:
956        # when it is set the answer is already known, so no file test is needed.
957
1369
18198
        if(!$self->{overwrite} && (-e $outfile || -l $outfile)) {
958
24
178
                $self->_croak_i18n('output_exists', { params => [$outfile] });
959        }
960
961        # Write into a temporary file next to the target; it is deleted
962        # automatically if anything below croaks
963
1345
4656
        my $tmp = File::Temp->new(DIR => $self->{output_dir}, TEMPLATE => $TEMP_TEMPLATE, UNLINK => 1);
964
1343
230292
        binmode $tmp, ':raw';
965
966
1343
53806
        if($self->{encoding} eq $ENC_CP1252) {
967
45
162
                $self->_export_transcoded($database, $table, $tmp);
968        } else {
969                # The BOM must reach the file before mdb-export starts writing to
970                # the same descriptor, hence the explicit flush
971
1298
19
1820
129
                print {$tmp} $UTF8_BOM if $self->{encoding} eq $ENC_UTF8_BOM;
972
1298
2954
                $tmp->flush() or $self->_croak_i18n('write_failed', { params => [$outfile, "$!"] });
973
1295
2814
                $self->_run_program($MDB_EXPORT, [$END_OF_OPTIONS, $database, $table], $tmp);
974        }
975
976
1298
7753
        $self->_install_file($tmp, $outfile);
977
978        # The file is now in place, so the table has been exported.  Row counts
979        # are optional extras: if counting fails it is only a warning (see
980        # _try_count_rows), and the export is logged without a count.
981
1291
2771
        my $rows = $self->{show_counts} ? $self->_try_count_rows($database, $table) : undef;
982
1291
1471
        if(defined $rows) {
983
31
434
                $self->_log(info => 'exported_rows', { params => [$table, $outfile, $rows], count => $rows });
984        } else {
985
1260
3316
                $self->_log(info => 'exported', { params => [$table, $outfile] });
986        }
987
1291
3322
        return $self;
988
34
34
34
7620
34
269
}
989
990# _export_transcoded
991# Purpose:        Export a table and convert it from UTF-8 to Windows-1252.
992# Entry Criteria: $out is an open, raw, writable filehandle.
993# Exit Status:    Returns $self; croaks on invalid UTF-8 or on a character
994#                 that has no cp1252 equivalent (rather than silently
995#                 replacing it with '?').
996# Side Effects:   Runs mdb-export into a second temporary file.
997sub _export_transcoded :Private {
998
51
2128
        my ($self, $database, $table, $out) = @_;
999
1000        # Reading the spool changes $. and the evals change $@; keep the
1001        # caller's values
1002
51
121
        local $.;
1003
51
66
        local $@;
1004
1005        # Spool to disk rather than memory, so huge tables do not exhaust RAM
1006
51
155
        my $spool = File::Temp->new(DIR => $self->{output_dir}, TEMPLATE => $TEMP_TEMPLATE, UNLINK => 1);
1007
51
7323
        binmode $spool, ':raw';
1008
51
1580
        $self->_run_program($MDB_EXPORT, [$END_OF_OPTIONS, $database, $table], $spool);
1009
43
515
        seek $spool, 0, 0;
1010
1011        # Convert line by line; $. gives the user a line number to look at
1012
43
7376
        while(my $line = <$spool>) {
1013
81
81
131
424
                my $chars = eval { $UTF8_CODEC->decode($line, FB_CROAK) };
1014
81
467
                $self->_croak_i18n('invalid_utf8', { params => [$table, $.] }) unless defined $chars;
1015
1016
74
74
84
350
                my $bytes = eval { $CP1252_CODEC->encode($chars, FB_CROAK) };
1017
74
180
                $self->_croak_i18n('unmappable', { params => [$table, $., $ENC_CP1252] }) unless defined $bytes;
1018
1019
63
63
68
290
                print {$out} $bytes;
1020        }
1021
1022        # Close the spool explicitly once it has been read.  (On a croak above,
1023        # File::Temp's destructor closes and deletes it.)
1024
25
110
        close $spool;
1025
25
1781
        return $self;
1026
34
34
34
6391
28
278
}
1027
1028# _install_file
1029# Purpose:        Move a finished temporary file to its final name.
1030# Entry Criteria: $tmp is a File::Temp holding the complete CSV.
1031# Exit Status:    Returns $self; croaks if the rename or chmod fails.
1032# Side Effects:   Replaces $outfile; the temporary file is no longer
1033#                 auto-deleted.
1034sub _install_file :Private {
1035
1293
11741
        my ($self, $tmp, $outfile) = @_;
1036
1037        # The eval must not overwrite the caller's $@
1038
1293
1501
        local $@;
1039
1040        # File::Temp creates files as 0600; give the CSV the permissions a
1041        # normal open() would have, i.e. 0666 less the umask
1042
1293
1332
        my $ok = eval {
1043
1293
3028
                close $tmp;
1044
1293
63644
                chmod $FILE_MODE & ~umask(), $tmp->filename();
1045
1293
80225
                rename $tmp->filename(), $outfile;
1046
1284
69405
                1;
1047        };
1048
1293
10044
        $self->_croak_i18n('write_failed', { params => [$outfile, _os_error($@)] }) unless $ok;
1049
1050
1284
2328
        $tmp->unlink_on_destroy(0);
1051
1284
6923
        return $self;
1052
34
34
34
4845
24
243
}
1053
1054# _count_rows
1055# Purpose:        Ask mdb-count how many rows a table has.
1056# Entry Criteria: $self->{programs}{'mdb-count'} is set.
1057# Exit Status:    Returns a non-negative integer; croaks if mdb-count fails.
1058# Side Effects:   Runs mdb-count.
1059sub _count_rows :Private {
1060
57
788
        my ($self, $database, $table) = @_;
1061
1062
57
140
        my $stdout = '';
1063
57
275
        $self->_run_program($MDB_COUNT, [$END_OF_OPTIONS, $database, $table], \$stdout);
1064
1065        # mdb-count prints just the number (perhaps with spaces around it).
1066        # Anything else - nothing, "-5", an error text - is not a count, and
1067        # must not quietly become 0: it is reported (as a warning, by
1068        # _try_count_rows)
1069
50
709
        my ($rows) = ($stdout // '') =~ /\A\s*(\d+)\s*\z/;
1070
50
266
        defined($rows) or $self->_croak_i18n('count_unreadable', { params => [$self->_printable(substr($stdout // '', 0, $COUNT_SHOWN))] });
1071
42
234
        return $rows;
1072
34
34
34
5365
46
277
}
1073
1074# _try_count_rows
1075# Purpose:        Count a table's rows, where a failure is only a warning.
1076#                 Row counts are optional extras: they must never make an
1077#                 exported table count as failed, nor end a dry run.
1078# Entry Criteria: $self->{programs}{'mdb-count'} is set.
1079# Exit Status:    Returns the count, or undef if it could not be had.
1080# Side Effects:   Runs mdb-count; on failure warns and logs "Cannot count
1081#                 the rows of T: E".  An interruption is not a failure to
1082#                 count: it is passed on, to stop the run.
1083sub _try_count_rows :Private {
1084
61
793
        my ($self, $database, $table) = @_;
1085
1086
61
115
        local $@;
1087
61
61
136
249
        my $rows = eval { $self->_count_rows($database, $table) };
1088
61
3376
        return $rows if defined $rows;
1089
1090
15
104
        die $@ if $self->{interrupted};
1091
14
15
        my $error = $@;
1092
14
21
        chomp $error;
1093
14
52
        $self->_warn('count_failed', { params => [$table, $error] });
1094
14
44
        return;
1095
34
34
34
4612
25
237
}
1096
1097# _run_program
1098# Purpose:        Run an mdbtools program and check that it succeeded.
1099#                 Shared by every call to mdbtools, so that failures are
1100#                 reported consistently.
1101# Entry Criteria: $name is a key of $self->{programs}; $args is an arrayref;
1102#                 $stdout is a scalar ref or a filehandle for run3().
1103# Exit Status:    Returns $self; croaks if the program exits non-zero or
1104#                 dies from a signal.
1105# Side Effects:   Runs a child process; writes to $stdout; sets $?.
1106sub _run_program :Private {
1107
1686
13860
        my ($self, $name, $args, $stdout) = @_;
1108
1109        # run3 sets $?, and reads captured output back from a temporary file,
1110        # which changes the handle $. refers to; keep the caller's values
1111
1686
3179
        local ($?, $.);
1112
1113        # Start the program in a clean environment (as perlsec asks, and as
1114        # taint mode requires): PATH keeps only absolute folders, and variables
1115        # that can change how a program is started are removed
1116        # (File::Spec->path and path_sep, not ":": Windows separates PATH with
1117        # ";" and its paths contain ":", as in C:\\)
1118
1686
31322
32891
21585
22433
50075
        local $ENV{PATH} = join($Config{path_sep}, map { _untaint($_) } grep { length && File::Spec->file_name_is_absolute($_) } File::Spec->path());
1119
1686
7029
        local @ENV{@UNSAFE_ENV};
1120
1686
21693
        delete @ENV{@UNSAFE_ENV};
1121
1122        # A list (not a string) is passed, so no shell ever sees the file or
1123        # table name and quoting cannot be abused
1124
1686
22459
        my $stderr = '';
1125
1686
1686
2213
3980
        run3([$self->{programs}{$name}, @{$args}], \undef, $stdout, \$stderr);
1126
1127        # Distinguish "never started" (-1) and a signal from an ordinary
1128        # non-zero exit status; $! only means something in the first case
1129
1680
95242573
        my ($status, $reason) = ($?, "$!");
1130
1680
2366
        chomp $stderr;
1131
1680
3047
        if($status == -1) {
1132
6
22
                $self->_croak_i18n('program_not_run', { params => [$name, $reason] });
1133        }
1134
1674
3174
        if($status & 127) {
1135                # While system() waits, Perl ignores INT and QUIT in this process,
1136                # so Ctrl-C shows up only as the child dying of SIGINT.  Treat
1137                # that as the user stopping the whole run, not as a bad table.
1138
14
235
                my $signal = (split ' ', $Config{sig_name})[$status & 127] // '';
1139
14
113
                if($signal eq 'INT' || $signal eq 'QUIT') {
1140
3
4
                        $self->{interrupted} = $signal;
1141
3
26
                        $self->_croak_i18n('interrupted', { params => [$signal] });
1142                }
1143
11
57
                $self->_croak_i18n('program_signalled', { params => [$name, $status & 127] });
1144        }
1145
1660
2615
        if($status) {
1146
33
324
                $self->_croak_i18n('program_failed', { params => [$name, $status >> 8, $stderr] });
1147        }
1148
1627
32276
        return $self;
1149
34
34
34
8190
29
265
}
1150
1151# _csv_filename
1152# Purpose:        Turn a table name into a safe, unique CSV file name.
1153# Entry Criteria: $table is a table name (possibly empty).
1154# Exit Status:    Returns a file name (no directory) ending in ".csv".
1155# Side Effects:   Records the name in $self->{used_names}.
1156#
1157# Names are compared case-insensitively, because Windows and macOS file
1158# systems are, so "Orders" and "ORDERS" do not overwrite each other.
1159# The loop (rather than a single suffix) guarantees uniqueness even when
1160# another table is literally called "Orders_2".
1161sub _csv_filename :Protected {
1162
17082
120594
        my ($self, $table) = @_;
1163
1164
17082
13101
        my $name = $table // '';
1165
1166        # Replace characters that are illegal somewhere, then strip leading
1167        # and trailing whitespace; Windows also silently drops trailing dots
1168
17082
18751
        $name =~ s/$UNSAFE_CHARS_RE/_/g;
1169
17082
21490
        $name =~ s/$BIDI_CONTROLS_RE/_/g;
1170
1171        # C1 controls (U+0080-U+009F; U+009B acts like ESC [ on terminals).
1172        # In a byte string they must be matched as their UTF-8 form, because a
1173        # bare [\x80-\x9F] would also hit the continuation bytes of ordinary
1174        # characters such as the Euro sign (E2 82 AC)
1175
17082
13567
        if(utf8::is_utf8($name)) {
1176
0
0
                $name =~ s/[\x{80}-\x{9F}]/_/g;
1177        } else {
1178
17082
11068
                $name =~ s/\xC2[\x80-\x9F]/_/g;
1179        }
1180
17082
11418
        $name =~ s/\A\s+//;
1181
17082
11414
        $name =~ s/[\s.]+\z//;
1182
1183        # Avoid hidden files, Windows device names and empty names
1184
17082
10166
        $name =~ s/\A\./_/;
1185
17082
21476
        $name = "_$name" if $name =~ $RESERVED_NAME_RE;
1186
17082
12448
        $name = $UNNAMED unless length $name;
1187
1188        # Find the smallest free suffix (_2, _3, ...).  Start from where this
1189        # name's last search ended rather than from 2: every suffix below that
1190        # point was taken then and is still taken (names are never released
1191        # during a run), so the answer is the same, but N tables with the same
1192        # name cost O(N) in total instead of O(N squared).
1193
17082
10634
        my $used = $self->{used_names};
1194
17082
10618
        my $base = lc $name;
1195
17082
10677
        my $file = $name . $CSV_SUFFIX;
1196
17082
13926
        if(exists $used->{lc $file}) {
1197
11697
10671
                my $n = $self->{next_suffix}{$base} // 2;
1198
11697
11335
                $n++ while exists $used->{lc "${name}_$n$CSV_SUFFIX"};
1199
11697
7142
                $file = "${name}_$n$CSV_SUFFIX";
1200
11697
8675
                $self->{next_suffix}{$base} = $n + 1;
1201        }
1202
17082
14944
        $used->{lc $file} = 1;
1203
1204
17082
26804
        return $file;
1205
34
34
34
9057
29
289
}
1206
1207# _dry_run
1208# Purpose:        Print the table -> file mapping without writing anything.
1209# Entry Criteria: $tables is the arrayref of selected tables.
1210# Exit Status:    Returns $self.
1211# Side Effects:   Prints to STDOUT; runs mdb-count when show_counts is on.
1212sub _dry_run :Private {
1213
66
1437
        my ($self, $database, $tables) = @_;
1214
1215        # Only show the ROWS column when counts are actually available
1216
66
197
        my $counts = $self->{show_counts};
1217
66
451
        my $format = $counts
1218                ? "%-${TABLE_COLUMN_WIDTH}s %${ROWS_COLUMN_WIDTH}s  %s\n"
1219                : "%-${TABLE_COLUMN_WIDTH}s %s\n";
1220
66
146
247
5694
        my @header = map { $self->i18n($_) } ($counts ? qw(column_table column_rows column_output) : qw(column_table column_output));
1221
1222        # Underline the title to the length of whatever the translation is
1223
66
3787
        my $title = $self->i18n('dry_run_title');
1224
66
5319
        print "\n$title\n", '=' x length($title), "\n\n";
1225
66
343
        printf $format, @header;
1226
66
251
        print '-' x $RULE_WIDTH, "\n";
1227
1228
66
66
96
137
        foreach my $table (@{$tables}) {
1229
89
154
                my @row = ($table);
1230                # A count that cannot be had is shown as "?" (with a warning), so a
1231                # dry run still lists every table and succeeds
1232
89
224
                push @row, $self->_try_count_rows($database, $table) // $UNKNOWN_COUNT if $counts;
1233                # The table name comes from the database, so show it escaped (the
1234                # file name is already safe; escaping it too costs nothing)
1235
89
426
                $row[0] = $self->_printable($row[0]);
1236
89
279
                printf $format, @row, $self->_printable($self->_csv_filename($table));
1237        }
1238
66
235
        print "\n";
1239
1240
66
140
        return $self;
1241
34
34
34
7111
32
304
}
1242
1243# _warn
1244# Purpose:        Warn the user and record the same text in the log.
1245# Entry Criteria: $key is a catalog key; $args an optional i18n() hashref.
1246# Exit Status:    Returns $self.
1247# Side Effects:   carp()s; logs at warn level.
1248sub _warn :Private {
1249
113
1318
        my ($self, $key, $args) = @_;
1250
1251
113
483
        $self->_carp_i18n($key, $args);
1252
113
344
        return $self->_log(warn => $key, $args);
1253
34
34
34
3670
30
224
}
1254
1255# _log
1256# Purpose:        Send a localised message to the logger, if there is one.
1257# Entry Criteria: $level is a logger method name (debug, info or warn).
1258# Exit Status:    Returns $self.
1259# Side Effects:   Calls $self->{logger}->$level().
1260sub _log :Private {
1261
1613
12740
        my ($self, $level, $key, $args) = @_;
1262
1263
1613
2891
        my $logger = $self->{logger} or return $self;
1264
1265        # Logging is secondary: a logger that dies (full disk, closed socket)
1266        # must not make a finished export look failed.  Say so once and stop
1267        # using it.
1268
154
173
        local $@;
1269        # Escaped, so a hostile table name cannot forge log lines (CR, LF)
1270        # or send escape sequences to whoever views the log
1271
154
154
149
192
709
20787
        if(!eval { $logger->$level($self->_printable($self->i18n($key, $args))); 1 }) {
1272
5
24
                my $error = $@;
1273
5
11
                chomp $error;
1274
5
13
                delete $self->{logger};
1275
5
16
                $self->_carp_i18n('log_failed', { params => [$error] });
1276        }
1277
154
261
        return $self;
1278
34
34
34
4740
24
235
}
1279
1280# _validation_reason
1281# Purpose:        Reduce a Params::Validate::Strict error to its meaning,
1282#                 e.g. "Parameter 'encoding' (latin1) must be one of utf8,
1283#                 utf8-bom, cp1252", dropping the module's own prefix
1284#                 ("Params::Validate::Strict line N: validate_strict: ")
1285#                 and the Perl location it appends.
1286# Entry Criteria: $error is the exception from validate_strict.
1287# Exit Status:    Returns a one-line string.
1288# Side Effects:   None.  A plain function, not a method.
1289sub _validation_reason :Private {
1290
92
46189
        my $error = shift;
1291
1292
92
108
        my $reason = "$error";
1293
92
247
        $reason =~ s/\AParams::Validate::Strict line \d+: //;
1294
92
145
        $reason =~ s/\Avalidate_strict: //;
1295
92
267
        $reason =~ s/ at \S+ line \d+\.?\n?\z//;
1296
92
96
        chomp $reason;
1297
92
288
        return $reason;
1298
34
34
34
4788
57
248
}
1299
1300# _untaint
1301# Purpose:        Mark a value that has already been validated as safe for
1302#                 taint mode.
1303# Entry Criteria: $value has been checked by the caller (see each call).
1304# Exit Status:    Returns the untainted value; returns $value unchanged if
1305#                 it is empty or contains a NUL (it will then fail safely
1306#                 at the operating system, still tainted).
1307# Side Effects:   None.  A plain function, not a method.
1308sub _untaint :Private {
1309
34244
117132
        my $value = shift;
1310
1311
34244
73155
        return ($value // '') =~ $UNTAINT_RE ? $1 : $value;
1312
34
34
34
3479
25
240
}
1313
1314# _os_error
1315# Purpose:        Get a readable OS error from an autodie exception or string.
1316# Entry Criteria: $error is whatever eval left in $@.
1317# Exit Status:    Returns a string.
1318# Side Effects:   None.  A plain function, not a method.
1319sub _os_error :Private {
1320
17
6281
        my $error = shift;
1321
1322        # autodie::exception keeps the original $! for us.  blessed(), not
1323        # ref(): asking an unblessed reference ->can() would die and hide
1324        # the real error.
1325
17
191
        my $text = (blessed($error) && $error->can('errno')) ? $error->errno() : "$error";
1326
17
50
        chomp $text;
1327
17
78
        return $text;
1328
34
34
34
3709
29
242
}
1329
13301;
1331