lib/Config/Abstraction.pm

Structural Coverage (Approximate)

TER1 (Statement): 92.56%
TER2 (Branch): 87.59%
TER3 (LCSAJ): 100.0% (67/67)
Approximate LCSAJ segments: 581

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package Config::Abstraction;
    2: 
    3: use strict;
    4: use warnings;
    5: 
    6: use Carp;
    7: use JSON::MaybeXS 'decode_json';	# Doesn't behave well with require
    8: use File::Slurp qw(read_file);
    9: use File::Spec;
   10: use Hash::Merge qw(merge);
   11: use Params::Get 0.15;
   12: use Params::Validate::Strict 0.37;
   13: use Scalar::Util;
   14: use Readonly;
   15: 
   16: # AES-256-GCM constants — do not change without updating _decrypt_enc_value and encrypt_value
   17: Readonly::Scalar my $_AES_NONCE_SIZE  => 12;	# GCM standard nonce length in bytes
   18: Readonly::Scalar my $_AES_TAG_SIZE    => 16;	# GCM authentication tag length in bytes
   19: Readonly::Scalar my $_AES_KEY_SIZE    => 32;	# AES-256 key length in bytes
   20: Readonly::Scalar my $_ENC_PAYLOAD_MIN => 28;	# shortest valid payload: nonce(12) + tag(16)
   21: Readonly::Scalar my $_HEX_KEY_LEN     => 64;	# 32 bytes expressed as hexadecimal
   22: Readonly::Scalar my $_B64_KEY_MIN     => 43;	# minimum base64/base64url chars for 32 bytes
   23: Readonly::Scalar my $_B64_KEY_MAX     => 44;	# maximum base64/base64url chars for 32 bytes
   24: 
   25: 
   26: =head1 NAME
   27: 
   28: Config::Abstraction - Merge and manage configuration data from different sources
   29: 
   30: =head1 VERSION
   31: 
   32: Version 0.40
   33: 
   34: =cut
   35: 
   36: our $VERSION = '0.40';
   37: 
   38: =head1 SYNOPSIS
   39: 
   40: =head2 Pattern 1: Environment overrides file overrides in-code defaults
   41: 
   42: The most common pattern for twelve-factor-style apps.
   43: The C<data> argument supplies defaults, a YAML file supplies site configuration,
   44: and environment variables allow per-deployment overrides without touching any file.
   45: 
   46:   # config/base.yaml
   47:   #   database:
   48:   #     host: db.example.com
   49:   #     port: 5432
   50:   #     user: app
   51: 
   52:   use Config::Abstraction;
   53: 
   54:   my $config = Config::Abstraction->new(
   55:       data        => { database => { host => 'localhost', port => 5432 } },
   56:       config_dirs => ['config'],
   57:       env_prefix  => 'APP_',
   58:   );
   59: 
   60:   # In production, set APP_DATABASE__HOST=db.prod.example.com in the environment.
   61:   # That silently overrides the file value, which in turn overrides the default.
   62:   my $host = $config->get('database.host');
   63:   my $port = $config->get('database.port');
   64: 
   65: =head2 Pattern 2: Command-line arguments override everything
   66: 
   67: Useful for CLI tools where operator flags must win over every other source.
   68: 
   69:   # Run as: myscript.pl --APP_LOGLEVEL=debug --APP_DATABASE__HOST=localhost
   70: 
   71:   use Config::Abstraction;
   72: 
   73:   my $config = Config::Abstraction->new(
   74:       config_dirs => ['config'],
   75:       env_prefix  => 'APP_',
   76:   );
   77: 
   78:   # @ARGV is consumed and stripped during new(); the resulting config already
   79:   # has cli-layer values at the highest precedence.
   80:   my $loglevel = $config->get('loglevel');       # 'debug'  (from --APP_LOGLEVEL)
   81:   my $db_host  = $config->get('database.host');  # 'localhost' (from --APP_DATABASE__HOST)
   82: 
   83: =head2 Pattern 3: Multi-file layering (base + local override)
   84: 
   85: Separates shared defaults from machine-specific tweaks.
   86: Every developer has a C<base.yaml>; only the production server has C<local.yaml>.
   87: 
   88:   # config/base.yaml   -- checked into version control
   89:   #   database:
   90:   #     host: localhost
   91:   #     user: dev
   92:   #
   93:   # config/local.yaml  -- NOT checked in; production only
   94:   #   database:
   95:   #     host: db.prod.example.com
   96:   #     user: produser
   97:   #     password: s3cr3t
   98: 
   99:   use Config::Abstraction;
  100: 
  101:   my $config = Config::Abstraction->new(
  102:       config_dirs => ['config'],   # loads base.yaml then local.yaml
  103:       env_prefix  => 'APP_',
  104:   );
  105: 
  106:   # On a dev machine (no local.yaml): host='localhost', user='dev'
  107:   # On the prod server:               host='db.prod.example.com', user='produser'
  108:   my $db = $config->get('database.host');
  109:   my $user = $config->get('database.user');
  110: 
  111: =head1 DESCRIPTION
  112: 
  113: C<Config::Abstraction> is a flexible configuration management layer that sits above C<Config::*> modules.
  114: It provides a simple way to layer multiple configuration sources with predictable merge order.
  115: It lets you define sources such as:
  116: 
  117: =over 4
  118: 
  119: =item * Perl hashes (in-memory defaults or dynamic values)
  120: 
  121: =item * Environment variables (with optional prefixes)
  122: 
  123: =item * Configuration files (YAML, JSON, INI, or plain key=value)
  124: 
  125: =item * Command-line arguments
  126: 
  127: =back
  128: 
  129: Sources are applied in the order they are provided. Later sources override
  130: earlier ones unless a key is explicitly set to C<undef> in the later source.
  131: 
  132: In addition to using drivers to load configuration data from multiple file
  133: formats (YAML, JSON, XML, and INI),
  134: it also allows levels of configuration, each of which overrides the lower levels.
  135: So, it also integrates environment variable
  136: overrides and command line arguments for runtime configuration adjustments.
  137: This module is designed to help developers manage layered configurations that can be loaded from files and overridden at run-time for debugging,
  138: offering a modern, robust and dynamic approach
  139: to configuration management.
  140: 
  141: =head2 Merge Precedence
  142: 
  143: Sources are applied in the order shown below.  Each row wins over every row
  144: above it.  When the same key appears in multiple sources, the highest-priority
  145: source always determines the final value - including when that value is C<undef>.
  146: 
  147:   Priority   Source                         Set with
  148:   --------   ------                         --------
  149:      1 (lo)  data constructor argument      data => { key => 'default' }
  150:      2        base.*  config files          config/base.yaml, base.json, ...
  151:      3        base.{env}.* config files     config/base.prod.yaml, ...
  152:      4        local.* config files          config/local.yaml, local.json, ...
  153:      5        local.{env}.* config files    config/local.prod.yaml, ...
  154:      6        default / script-name files   config/default.yaml, myapp.yaml, ...
  155:      7        config_file / config_files    config_file => '/etc/myapp.yaml'
  156:      8        Environment variables         APP_DATABASE__HOST=db.prod.example.com
  157:      9 (hi)  CLI arguments (@ARGV)          --APP_DATABASE__HOST=db.prod.example.com
  158: 
  159: The active environment is set by the C<environment> constructor option, or
  160: auto-detected from C<{env_prefix}ENV> (e.g. C<APP_ENV>), C<PLACK_ENV>, or
  161: C<NODE_ENV>.  When no environment is configured, rows 3 and 5 are skipped.
  162: 
  163: Within the file tier (rows 2-7), later files override earlier files using
  164: C<Hash::Merge> LEFT_PRECEDENT: every key in the later file wins over the same
  165: key in an earlier file, even when the later value is C<undef> (YAML C<~>).
  166: Nested hashes are merged recursively, so a C<local.yaml> that only sets
  167: C<database.host> will not erase C<database.port> from C<base.yaml>.
  168: 
  169:   Example - what wins for the key C<database.host> when APP_ENV=prod:
  170: 
  171:   data              =>  'localhost'              (overridden by base.yaml)
  172:   base.yaml         =>  'db.example.com'         (overridden by base.prod.yaml)
  173:   base.prod.yaml    =>  'db.prod.example.com'    (overridden by local.yaml)
  174:   local.yaml        =>  'db.local.example.com'   (overridden by local.prod.yaml)
  175:   local.prod.yaml   =>  'db.prod-local.example.com'  (overridden by $APP_DATABASE__HOST)
  176:   $APP_DATABASE__HOST  =>  'db.override.example.com'    <-- this wins
  177: 
  178: =head2 KEY FEATURES
  179: 
  180: =over 4
  181: 
  182: =item * Multi-Format Support
  183: 
  184: Supports configuration files in YAML, JSON, XML, and INI formats.
  185: Automatically merges configuration data from these different formats,
  186: allowing hierarchical configuration management.
  187: 
  188: =item * Environment Variable Overrides
  189: 
  190: Allows environment variables to override values in the configuration files.
  191: By setting environment variables with a specific prefix (default: C<APP_>),
  192: values in the configuration files can be dynamically adjusted without modifying
  193: the file contents.
  194: 
  195: =item * Flattened Configuration Option
  196: 
  197: Optionally supports flattening the configuration structure. This converts deeply
  198: nested configuration keys into a flat key-value format (e.g., C<database.user>
  199: instead of C<database-E<gt>{user}>). This makes accessing values easier for
  200: applications that prefer flat structures or need compatibility with flat
  201: key-value stores.
  202: 
  203: =item * Layered Configuration
  204: 
  205: Supports merging multiple layers of configuration files. For example, you can
  206: have a C<base.yaml> configuration file that provides default values, and a
  207: C<local.yaml> (or C<local.json>, C<local.xml>, etc.) file that overrides
  208: specific values. This allows for environment-specific configurations while
  209: keeping defaults intact.
  210: 
  211: =item * Merge Strategy
  212: 
  213: The module merges the configuration data intelligently, allowing values in more
  214: specific files (like C<local.yaml>, C<local.json>, C<local.xml>, C<local.ini>)
  215: to override values in base files. This enables a flexible and layered configuration
  216: system where you can set defaults and override them for specific environments.
  217: 
  218: =item * Error Handling
  219: 
  220: Includes error handling for loading configuration files.
  221: If any file fails to
  222: load (e.g., due to syntax issues), the module will throw descriptive error
  223: messages to help with debugging.
  224: 
  225: =item * Centralized Configuration Management
  226: 
  227: Supports remote configuration management files,
  228: so that configuration on remote machines can be centrally managed.
  229: 
  230: =back
  231: 
  232: =head2 SUPPORTED FILE FORMATS
  233: 
  234: =over 4
  235: 
  236: =item * YAML (C<*.yaml>, C<*.yml>)
  237: 
  238: Loaded with C<YAML::XS>.
  239: 
  240: =item * JSON (C<*.json>)
  241: 
  242: Loaded with C<JSON::MaybeXS>.
  243: 
  244: =item * XML (C<*.xml>)
  245: 
  246: Loaded with C<XML::Simple> (preferred) or C<XML::PP> (fallback).
  247: 
  248: =item * INI (C<*.ini>)
  249: 
  250: Loaded with C<Config::IniFiles>.
  251: 
  252: =item * TOML (C<*.toml>)
  253: 
  254: Loaded with C<TOML::Tiny>, which implements TOML 1.0.  TOML is a good choice
  255: for human-editable configuration because its syntax is unambiguous: all values
  256: are typed, strings must be quoted, and nesting uses C<[section]> headers or
  257: dotted keys rather than indentation.
  258: 
  259:   # Example: config/base.toml
  260:   [database]
  261:   host   = "db.example.com"
  262:   port   = 5432
  263:   user   = "app"
  264: 
  265:   [cache]
  266:   ttl    = 300
  267:   debug  = false
  268: 
  269: C<base.toml> and C<local.toml> are discovered automatically in C<config_dirs>;
  270: C<local.toml> has higher precedence than C<base.toml>, following the same
  271: layering rules as all other formats.  TOML is also tried as a fallback parser
  272: in the all-parsers chain used for extensionless C<config_file> entries.
  273: 
  274: If C<TOML::Tiny> is not installed, TOML files are silently skipped with a
  275: C<carp> warning.
  276: 
  277: =back
  278: 
  279: =head2 ENVIRONMENT VARIABLE HANDLING
  280: 
  281: Configuration values can be overridden via environment variables. Environment variables use double underscores (__) to denote nested configuration keys and single underscores remain as part of the key name under the prefix namespace.
  282: 
  283: For example:
  284: 
  285:   APP_DATABASE__USER becomes database.user (nested structure)
  286: 
  287:     $ export APP_DATABASE__USER="env_user"
  288: 
  289: will override any value set for `database.user` in the configuration files.
  290: 
  291:   APP_LOGLEVEL becomes APP.loglevel (flat under prefix namespace)
  292: 
  293:   APP_API__RATE_LIMIT becomes api.rate_limit (mixed usage)
  294: 
  295: This allows you to override both top-level and nested configuration values using environment variables.
  296: 
  297: Configuration values can be overridden via the command line (C<@ARGV>).
  298: For instance, if you have a key in the configuration such as C<database.user>,
  299: you can override it by adding C<"--APP_DATABASE__USER=other_user_name"> to the command line arguments.
  300: This will override any value set for C<database.user> in the configuration files.
  301: 
  302: =head2 EXAMPLE CONFIGURATION FLOW
  303: 
  304: =over 4
  305: 
  306: =item 1. Data Argument
  307: 
  308: The data passed into the constructor via the C<data> argument is the starting point.
  309: Essentially,
  310: this contains the default values.
  311: 
  312: =item 2. Loading Files
  313: 
  314: The module then looks for configuration files in the specified directories.
  315: It loads the following files in order of preference:
  316: C<base.yaml>, C<local.yaml>, C<base.json>, C<local.json>, C<base.xml>,
  317: C<local.xml>, C<base.ini>, and C<local.ini>.
  318: 
  319: If C<config_file> or C<config_files> is set, those files are loaded last.
  320: 
  321: If no C<config_dirs> is given, try hard to find the files in various places.
  322: 
  323: The value of C<config_dirs> can be overridden at runtime by the environment variable CONFIG_DIR
  324: (note that is just one directory, hence it's CONFIG_DIR not CONFIG_DIRS).
  325: 
  326: =item 3. Merging and Resolving
  327: 
  328: The module merges the contents of these files, with more specific configurations
  329: (e.g., C<local.*>) overriding general ones (e.g., C<base.*>).
  330: 
  331: =item 4. Environment Overrides
  332: 
  333: After loading and merging the configuration files,
  334: the environment variables are
  335: checked and used to override any conflicting settings.
  336: 
  337: =item 5. Command Line
  338: 
  339: Next, the command line arguments are checked and used to override any conflicting settings.
  340: 
  341: =item 6. Accessing Values
  342: 
  343: Values in the configuration can be accessed using a dotted notation
  344: (e.g., C<'database.user'>), regardless of the file format used.
  345: 
  346: =back
  347: 
  348: =head1 METHODS
  349: 
  350: =head2 new
  351: 
  352: Constructor for creating a new configuration object.
  353: 
  354: Options:
  355: 
  356: =over 4
  357: 
  358: =item * C<config_dirs>
  359: 
  360: An arrayref of directories to look for configuration files
  361: (default: C<$CONFIG_DIR>, C<$HOME/.conf>, C<$HOME/config>, C<$HOME/conf>, C<$DOCUMENT_ROOT/conf>, C<$DOCUMENT_ROOT/../conf>, C<conf>).
  362: 
  363: For centralised configuration management,
  364: entries beginning with C</../> are treated as remote specifications using the
  365: Newcastle Connection convention -- see L</Remote configuration directories (Newcastle Connection)>.
  366: 
  367: =item * C<config_file>
  368: 
  369: Points to a configuration file of any format.
  370: 
  371: =item * C<config_files>
  372: 
  373: An arrayref of files to look for in the configuration directories.
  374: Put the more important files later,
  375: since later files override earlier ones.
  376: 
  377: Considers the files C<default> and C<$script_name> before looking at C<config_file> and C<config_files>.
  378: 
  379: =item * C<data>
  380: 
  381: A hash ref of default data to prime the configuration with.
  382: These are applied before loading
  383: other sources and can be overridden by later sources or by explicitly passing
  384: options directly to C<new>.
  385: 
  386:   $config = Config::Abstraction->new(
  387:       data => {
  388:           log_level => 'info',
  389:           retries => 3,
  390:       }
  391:   );
  392: 
  393: =item * C<defaults>
  394: 
  395: A hash reference that provides default values for the object's own attributes (such as C<config_dirs>, C<logger>, C<flatten>, etc.).
  396: If this option is supplied,
  397: the object is initialized using the keys in this hash as the base;
  398: any other options passed directly to C<new()> (aside from C<env_prefix>) are ignored.
  399: This allows you to pre-define a standard configuration profile for the object itself.
  400: Note that C<defaults> is distinct from the C<data> option - C<data> supplies the initial configuration values that will be merged with files, environment, and command line,
  401: while C<defaults> sets the object's internal parameters.
  402: The C<env_prefix> value,
  403: if provided as a top-level argument,
  404: still takes precedence over any C<env_prefix> that might exist inside the C<defaults> hash.
  405: 
  406: =item * C<encryption_key>
  407: 
  408: A 256-bit (32-byte) AES key used to transparently decrypt C<ENC[...]> values found in any
  409: configuration source after the full merge.  The key may be supplied as:
  410: 
  411: =over 4
  412: 
  413: =item 32 raw bytes
  414: 
  415: =item 64 lowercase or uppercase hex characters
  416: 
  417: =item 44 Base64 or Base64url characters (standard or URL-safe alphabet, with or without trailing C<=>)
  418: 
  419: =back
  420: 
  421: If no key is configured (and neither C<encryption_key_file> nor the corresponding
  422: environment variables are set), C<ENC[...]> tokens are left as literal strings.
  423: Decryption requires L<CryptX> (C<Crypt::AuthEnc::GCM>); if that module is absent a
  424: C<croak> is raised when an encrypted value is encountered.
  425: 
  426: See L</ENCRYPTED VALUES> for the full workflow.
  427: 
  428: =item * C<encryption_key_file>
  429: 
  430: Path to a file whose first line contains the encryption key in any of the formats accepted
  431: by C<encryption_key>.  Takes precedence over the C<ENCRYPTION_KEY_FILE> and
  432: C<{env_prefix}ENCRYPTION_KEY_FILE> environment variables, but is overridden by a key
  433: supplied directly via C<encryption_key>.
  434: 
  435: =item * C<env_prefix>
  436: 
  437: A prefix for environment variable keys and comment line options, e.g. C<MYAPP_DATABASE__USER>,
  438: (default: C<'APP_'>).
  439: 
  440: =item * C<environment>
  441: 
  442: The name of the active deployment environment (e.g. C<'dev'>, C<'staging'>, C<'prod'>).
  443: When set, config files named C<base.{env}.*> are loaded immediately after their C<base.*>
  444: equivalents, and C<local.{env}.*> files are loaded immediately after C<local.*> files,
  445: giving two additional override tiers at no cost to the base configuration.
  446: 
  447:   my $cfg = Config::Abstraction->new(
  448:       config_dirs => ['config'],
  449:       environment => 'prod',          # loads base.prod.yaml, local.prod.yaml, ...
  450:   );
  451: 
  452: If C<environment> is not given, the value is auto-detected in this order:
  453: 
  454: =over 4
  455: 
  456: =item 1. C<{env_prefix}ENV> environment variable (e.g. C<APP_ENV>)
  457: 
  458: =item 2. C<PLACK_ENV>
  459: 
  460: =item 3. C<NODE_ENV>
  461: 
  462: =back
  463: 
  464: When none of these is set, the environment-specific tiers are silently skipped.
  465: The environment name must contain only ASCII letters, digits, hyphens, and underscores
  466: (matched against C</^[A-Za-z0-9_\-]+$/>); any other value causes a C<croak>.
  467: 
  468: =item * C<file>
  469: 
  470: Synonym for C<config_file>
  471: 
  472: =item * C<flatten>
  473: 
  474: If true, returns a flat hash structure like C<{database.user}> (default: C<0>) instead of C<{database}{user}>.
  475: `
  476: =item * C<level>
  477: 
  478: Level for logging.
  479: 
  480: =item * C<logger>
  481: 
  482: Used for warnings and traces.
  483: It can be an object that understands warn() and trace() messages,
  484: such as a L<Log::Log4perl> or L<Log::Any> object,
  485: a reference to code,
  486: a reference to an array,
  487: or a filename.
  488: 
  489: =item * C<path>
  490: 
  491: A synonym of C<config_dirs>.
  492: 
  493: =item * C<sep_char>
  494: 
  495: The separator in keys.
  496: The default is a C<'.'>,
  497: as in dotted notation,
  498: such as C<'database.user'>.
  499: 
  500: =item * C<schema>
  501: 
  502: A L<Params::Validate::Strict> compatible schema to validate the configuration file against.
  503: 
  504: =item * C<validators>
  505: 
  506: A hashref mapping dotted config keys to validation rules.  Each rule is applied to the
  507: corresponding value in the merged configuration immediately after all sources have been
  508: merged (or on first access when C<lazy> is set).  A validation failure croaks.
  509: 
  510: Each rule may be one of:
  511: 
  512: =over 4
  513: 
  514: =item A type name string
  515: 
  516:   validators => {
  517:       'database.port' => 'integer',
  518:       'app.name'      => 'string',
  519:       'price'         => 'number',
  520:       'enabled'       => 'boolean',
  521:       'tags'          => 'array',
  522:       'settings'      => 'hash',
  523:   }
  524: 
  525: Supported types: C<integer> (matches C</^-?\d+$/> -- no decimal point),
  526: C<number> or C<float> (C<Scalar::Util::looks_like_number>), C<boolean>
  527: (C<0>, C<1>, C<true>, C<false>, C<yes>, C<no> -- case-insensitive),
  528: C<string> (any defined non-reference scalar), C<array> (arrayref),
  529: C<hash> (hashref).
  530: 
  531: =item A compiled regular expression
  532: 
  533:   validators => {
  534:       'log.level' => qr/^(?:debug|info|warn|error|fatal)$/i,
  535:       'app.name'  => qr/^\w[\w\-]{1,63}$/,
  536:   }
  537: 
  538: The value must be defined and must match the regex.
  539: 
  540: =item A coderef
  541: 
  542:   validators => {
  543:       'database.port' => sub { my $v = shift; defined($v) && $v >= 1 && $v <= 65535 },
  544:   }
  545: 
  546: Called with the value as its only argument.  Must return a true value; otherwise the
  547: constructor croaks.
  548: 
  549: =item A hashref combining multiple constraints
  550: 
  551:   validators => {
  552:       'database.port' => {
  553:           type     => 'integer',
  554:           min      => 1,
  555:           max      => 65535,
  556:           required => 1,
  557:       },
  558:       'api.key' => {
  559:           pattern  => qr/^[A-Za-z0-9]{32}$/,
  560:           required => 1,
  561:       },
  562:   }
  563: 
  564: Keys: C<type> (type-name string as above), C<pattern> (compiled regex), C<min> (numeric
  565: lower bound, inclusive), C<max> (numeric upper bound, inclusive), C<required> (if true,
  566: the key must exist and its value must be defined).
  567: 
  568: =back
  569: 
  570: All constraint types may be freely combined: specifying both C<type> and C<pattern>
  571: requires the value to satisfy both.
  572: 
  573: =item * C<checker>
  574: 
  575: A prototype string (YAML) or hashref passed to L<Config::Checker> for template-based
  576: structural validation.  C<Config::Checker> must be installed; if it is absent a C<carp>
  577: warning is emitted and validation is skipped.
  578: 
  579: The prototype mirrors the expected config structure.  Keys and values can carry type
  580: annotations (C<[INTEGER]>, C<[PATH]>, C<[HOSTNAME]>, ...), custom code checks
  581: (C<{...}>), and quantity specifiers (C<?>: optional, C<+>: one or more, C<*>: zero or
  582: more):
  583: 
  584:   my $config = Config::Abstraction->new(
  585:       config_dirs => ['config'],
  586:       checker     => <<'END_PROTOTYPE',
  587:   database:
  588:     host: hostname of the database server[HOSTNAME]
  589:     port: '?<5432>port number[INTEGER]'
  590:     user: database username
  591:   END_PROTOTYPE
  592:   );
  593: 
  594: See L<Config::Checker> for the full prototype syntax.
  595: 
  596: =item * C<lazy>
  597: 
  598: When set to a true value, all source discovery and file I/O are deferred until the first
  599: call to C<get()>, C<exists()>, C<all()>, C<explain_sources()>, C<prefer_*()>,
  600: C<merge_defaults()>, or any AUTOLOAD accessor.  This can reduce startup time in
  601: applications that construct a C<Config::Abstraction> object before they know whether
  602: they will access it.
  603: 
  604: B<Differences from the default (eager) behaviour, and the debugging problems they cause:>
  605: 
  606: =over 4
  607: 
  608: =item 1. C<new()> always succeeds, even when no configuration exists.
  609: 
  610: In eager mode, C<new()> returns C<undef> immediately when no files are found and no
  611: C<data> was supplied, so a missing config is caught at the point of construction.
  612: In lazy mode, C<new()> always returns a blessed object.  If the configuration
  613: directories do not exist, the files are missing, or the prefix is wrong, you will
  614: not find out until the first accessor call - which may be deep inside your business
  615: logic, far from where the object was created.
  616: 
  617:   # Problem: the typo in config_dirs goes unnoticed until runtime
  618:   my $cfg = Config::Abstraction->new(
  619:       config_dirs => ['/etc/myapp/conifg'],   # typo - directory does not exist
  620:       lazy => 1,
  621:   );
  622:   # ... many lines later ...
  623:   my $host = $cfg->get('database.host');      # silently returns undef here
  624: 
  625: =item 2. Schema validation errors appear at the wrong place in the call stack.
  626: 
  627: When a C<schema> is supplied, eager mode validates the merged configuration inside
  628: C<new()> and throws immediately if the config does not match.  In lazy mode,
  629: validation is deferred to the first accessor call.  A schema violation therefore
  630: surfaces as an exception thrown by C<get()> or C<all()>, not by C<new()>, and the
  631: stack trace points to the accessor call rather than the construction site.  If the
  632: object is created in one module and accessed in another, the error message can be
  633: very misleading.
  634: 
  635: =item 3. Environment variables and C<@ARGV> are captured at access time, not construction time.
  636: 
  637: The module reads C<%ENV> and C<@ARGV> during C<_load_config()>.  In eager mode that
  638: happens in C<new()>, so the configuration reflects the environment at the moment the
  639: object is built.  In lazy mode, any change to C<%ENV> or C<@ARGV> between C<new()>
  640: and the first accessor call is silently picked up.  This makes test isolation harder:
  641: setting an environment variable I<after> constructing a lazy object will affect the
  642: values it returns, which is not the case with an eager object.
  643: 
  644: =item 4. File system state may change between construction and first use.
  645: 
  646: Because lazy mode defers all I/O, the files that are read are those that exist at the
  647: moment of first access, not at the moment C<new()> is called.  If a config file is
  648: written, deleted, or replaced between those two points (for example, by another
  649: process or test fixture), the object will silently use the new state.  An eager object
  650: is immune to this race.
  651: 
  652: =item 5. Errors from format parsers appear at accessor call sites.
  653: 
  654: Malformed YAML, JSON, XML, or INI files emit a C<carp> warning and are skipped during
  655: loading.  In eager mode those warnings appear near the program's startup.  In lazy mode
  656: they appear during the first accessor call, which can make it look as though a routine
  657: in your business logic is generating configuration warnings.
  658: 
  659: =back
  660: 
  661: B<When to use lazy loading:> It is most useful when a C<Config::Abstraction> object is
  662: created speculatively at module-load time and may never be accessed (for example, in a
  663: web framework where the config object is built for every request but some request paths
  664: never read it).  Avoid it when startup correctness is important, when you rely on
  665: C<new()> returning C<undef> to detect missing configuration, or when you use schema
  666: validation and need errors to point at the construction site.
  667: 
  668: =back
  669: 
  670: If just one argument is given, it is assumed to be the name of a file.
  671: 
  672: =cut
  673: 
  674: sub new
  675: {
676 → 679 → 686  676: 	my $class = shift;
  677: 	my $params;
  678: 
  679: 	if(scalar(@_) == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

680: # Just one parameter - the name of a file 681: $params = Params::Get::get_params('file', \@_); 682: } else { 683: $params = Params::Get::get_params(undef, \@_) || {}; 684: } 685: 686 → 692 → 721 686: $params->{'config_dirs'} //= $params->{'path'}; # Compatibility with Config::Auto 687: 688: $params->{config_dirs} = [ $ENV{CONFIG_DIR} ] if(defined($ENV{CONFIG_DIR})); 689: 690: $params->{'config_file'} //= $params->{'file'} if($params->{'file'}); 691: 692: if(!defined($params->{'config_dirs'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

693: if($params->{'config_file'} && File::Spec->file_name_is_absolute($params->{'config_file'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

694: $params->{'config_dirs'} = ['']; 695: } else { 696: # Set up the default value for config_dirs 697: if($^O ne 'MSWin32') {

Mutants (Total: 1, Killed: 0, Survived: 1)
698: $params->{'config_dirs'} = [ '/etc', '/usr/local/etc' ]; 699: } else { 700: $params->{'config_dirs'} = ['']; 701: } 702: if($ENV{'HOME'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

703: push @{$params->{'config_dirs'}}, 704: File::Spec->catdir($ENV{'HOME'}, '.conf'), 705: File::Spec->catdir($ENV{'HOME'}, '.config'), 706: File::Spec->catdir($ENV{'HOME'}, 'conf'), 707: } elsif($ENV{'DOCUMENT_ROOT'}) { 708: push @{$params->{'config_dirs'}}, 709: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, File::Spec->updir(), 'conf'), 710: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, 'conf'), 711: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, 'config'); 712: } 713: if(my $dir = $ENV{'CONFIG_DIR'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

714: push @{$params->{'config_dirs'}}, $dir; 715: } else { 716: push @{$params->{'config_dirs'}}, 'conf', 'config'; 717: } 718: } 719: } 720: 721 → 732 → 753 721: my $self = bless { 722: sep_char => '.', 723: %{$params->{defaults} ? $params->{defaults} : $params}, 724: env_prefix => $params->{env_prefix} || 'APP_', 725: config => {}, 726: }, $class; 727: 728: # Cache the compiled sep_char regex once so get() and exists() do not 729: # recompile it on every key lookup. 730: $self->{'_sep_re'} = qr/\Q$self->{'sep_char'}\E/; 731: 732: if(my $logger = $self->{'logger'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

733: if(!Scalar::Util::blessed($logger)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

734: # Don't call $self->_load_driver('Log::Abstraction') as it can make a call to logger, which is yet to be set up 735: eval "require Log::Abstraction"; 736: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

737: carp(ref($self), ": Log::Abstraction failed to load: $@, disabling logging"); 738: if(ref($logger) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

739: push @{$logger}, "Log::Abstraction failed to load: $@, disabling logging"; 740: } 741: $self->{'logger'} = undef; # disable: unblessed ref would fatal on method calls 742: } else { 743: Log::Abstraction->import(); 744: $self->{'logger'} = Log::Abstraction->new($logger); 745: if($params->{'level'} && $self->{'logger'}->can('level')) {

Mutants (Total: 1, Killed: 0, Survived: 1)
746: $self->{'logger'}->level($params->{'level'}); 747: } 748: } 749: } 750: } 751: # Fail early when an explicitly-specified key file is missing rather than 752: # silently falling back to unencrypted operation (env-var key files are optional). 753 → 753 → 758 753: if(defined $self->{'encryption_key_file'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

754: my $kf = $self->{'encryption_key_file'}; 755: Carp::croak(ref($self) . ": encryption_key_file '$kf' does not exist") unless -f $kf; 756: } 757: 758 → 758 → 767 758: if($self->{'lazy'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

759: # Defer all source scanning to the first accessor call. 760: # Stash validators/checker/schema for later (after _load_config runs). 761: $self->{'_lazy_schema'} = delete $self->{'schema'} if $self->{'schema'}; 762: $self->{'_lazy_validators'} = delete $self->{'validators'} if $self->{'validators'}; 763: $self->{'_lazy_checker'} = delete $self->{'checker'} if $self->{'checker'}; 764: return $self;

Mutants (Total: 2, Killed: 2, Survived: 0)

765: } 766: 767 → 769 → 772 767: $self->_load_config(); 768: 769: if(my $schema = $params->{'schema'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

770: $self->{'config'} = Params::Validate::Strict::validate_strict(schema => $schema, input => $self->{'config'}); 771: } 772 → 772 → 775 772: if(my $validators = $params->{'validators'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

773: $self->_run_validators($validators); 774: } 775 → 775 → 779 775: if(my $checker = $params->{'checker'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

776: $self->_run_checker($checker); 777: } 778: 779 → 779 → 782 779: if(defined($self->{'config'}) && scalar(keys %{$self->{'config'}})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

780: return $self;

Mutants (Total: 2, Killed: 2, Survived: 0)

781: } 782: return undef;

Mutants (Total: 2, Killed: 2, Survived: 0)

783: } 784: 785: # Trigger deferred loading when the object was constructed with lazy => 1. 786: # Safe to call unconditionally: it is a no-op once loading has completed. 787: sub _ensure_loaded 788: { 789 → 793 → 796 789: my $self = shift; 790: return unless $self->{'lazy'}; # fast-path: not a lazy object 791: delete $self->{'lazy'}; # prevent re-entry on recursive calls 792: $self->_load_config(); 793: if(my $schema = delete $self->{'_lazy_schema'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

794: $self->{'config'} = Params::Validate::Strict::validate_strict(schema => $schema, input => $self->{'config'}); 795: } 796 → 796 → 799 796: if(my $validators = delete $self->{'_lazy_validators'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

797: $self->_run_validators($validators); 798: } 799 → 799 → 0 799: if(my $checker = delete $self->{'_lazy_checker'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
800: $self->_run_checker($checker); 801: } 802: } 803: 804: # Resolve the active deployment environment name. 805: # Checked in order: 'environment' constructor option, {prefix}ENV env var, 806: # PLACK_ENV, NODE_ENV. Returns undef when none is set. 807: # The value must match /^[A-Za-z0-9_\-]+$/ or a croak is raised. 808: sub _get_environment 809: { 810 → 814 → 823 810: my $self = shift; 811: 812: my $env; 813: 814: if(defined($self->{'environment'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

815: $env = $self->{'environment'}; 816: } else { 817: my $prefix = $self->{'env_prefix'} // 'APP_'; 818: $env = $ENV{$prefix . 'ENV'} 819: // $ENV{'PLACK_ENV'} 820: // $ENV{'NODE_ENV'}; 821: } 822: 823 → 826 → 830 823: return undef unless defined $env;

Mutants (Total: 2, Killed: 2, Survived: 0)

824: return undef if $env eq '';

Mutants (Total: 2, Killed: 2, Survived: 0)

825: 826: unless($env =~ /^[A-Za-z0-9_-]+$/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

827: Carp::croak(ref($self) . ": invalid environment name '$env': must contain only alphanumerics, hyphens, or underscores"); 828: } 829: 830: return $env;

Mutants (Total: 2, Killed: 2, Survived: 0)

831: } 832: 833: # _sanitize_yaml_values: Recursively replace CODE and GLOB refs with undef. 834: # YAML::XS can produce CODE refs from !!perl/code tags regardless of DisableCode. 835: # Applying this after LoadFile prevents executable references from reaching callers. 836: sub _sanitize_yaml_values 837: { 838 → 841 → 844 838: my ($self, $val) = @_; 839: my $r = ref($val); 840: return undef if $r eq 'CODE' || $r eq 'GLOB';

Mutants (Total: 2, Killed: 0, Survived: 2)
841: if($r eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

842: return { map { $_ => $self->_sanitize_yaml_values($val->{$_}) } keys %$val }; 843: } 844 → 844 → 847 844: if($r eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

845: return [ map { $self->_sanitize_yaml_values($_) } @$val ]; 846: } 847: return $val;

Mutants (Total: 2, Killed: 2, Survived: 0)

848: } 849: 850: # Resolve the AES-256 encryption key from constructor option, env vars, or key file. 851: # Returns 32 raw bytes, or undef when no key is configured. 852: sub _get_encryption_key 853: { 854 → 858 → 863 854: my $self = shift; 855: 856: my $raw = $self->{'encryption_key'}; 857: 858: if(!defined($raw)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

859: my $prefix = $self->{'env_prefix'} // 'APP_'; 860: $raw = $ENV{$prefix . 'ENCRYPTION_KEY'} // $ENV{'ENCRYPTION_KEY'}; 861: } 862: 863 → 863 → 877 863: if(!defined($raw)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

864: my $prefix = $self->{'env_prefix'} // 'APP_'; 865: my $kf = $self->{'encryption_key_file'} 866: // $ENV{$prefix . 'ENCRYPTION_KEY_FILE'} 867: // $ENV{'ENCRYPTION_KEY_FILE'}; 868: if(defined($kf) && -f $kf) {

Mutants (Total: 1, Killed: 1, Survived: 0)

869: open my $fh, '<', $kf 870: or Carp::croak(ref($self) . ": cannot open encryption_key_file '$kf': $!"); 871: $raw = <$fh>; 872: close $fh; 873: chomp $raw if defined $raw; 874: } 875: } 876: 877: return undef unless defined $raw;

Mutants (Total: 2, Killed: 2, Survived: 0)

878: return $self->_decode_encryption_key($raw);

Mutants (Total: 2, Killed: 2, Survived: 0)

879: } 880: 881: # Accept a key as 32 raw bytes, 64 hex chars, or 43-44 base64/base64url chars. 882: # Returns 32 raw bytes or croaks. 883: sub _decode_encryption_key 884: { 885 → 889 → 893 885: my ($self, $raw) = @_; 886: 887: return $raw if length($raw) == $_AES_KEY_SIZE;

Mutants (Total: 3, Killed: 3, Survived: 0)

888: 889: if(length($raw) == $_HEX_KEY_LEN && $raw =~ /^[0-9A-Fa-f]{$_HEX_KEY_LEN}$/) {

Mutants (Total: 2, Killed: 2, Survived: 0)

890: return pack('H*', $raw);

Mutants (Total: 2, Killed: 2, Survived: 0)

891: } 892: 893 → 893 → 901 893: if(length($raw) >= $_B64_KEY_MIN && length($raw) <= $_B64_KEY_MAX) {

Mutants (Total: 7, Killed: 7, Survived: 0)

894: require MIME::Base64; 895: (my $b64 = $raw) =~ tr/-_/+\//; # base64url → base64 896: $b64 .= '=' x ((4 - length($b64) % 4) % 4); 897: my $bytes = MIME::Base64::decode_base64($b64); 898: return $bytes if length($bytes) == $_AES_KEY_SIZE;

Mutants (Total: 3, Killed: 3, Survived: 0)

899: } 900: 901: Carp::croak(ref($self) . ': encryption_key must be ' . $_AES_KEY_SIZE . ' raw bytes, ' . $_HEX_KEY_LEN . ' hex chars, or ' . $_B64_KEY_MAX . ' base64 chars (got length ' . length($raw) . ')'); 902: } 903: 904: # Recursively walk $href and decrypt any leaf value matching ENC[...]. 905: # $seen guards against circular references (possible when Hash::Merge 906: # no-clone mode is active and sub-hashes are shared across the merge tree). 907: sub _decrypt_config_values 908: { 909 → 913 → 0 909: my ($self, $href, $key, $seen) = @_; 910: $seen //= {}; 911: return if $seen->{Scalar::Util::refaddr($href)}++; 912: 913: for my $k (keys %{$href}) { 914: next if $k eq 'config_path'; 915: my $v = $href->{$k}; 916: if(ref($v) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

917: $self->_decrypt_config_values($v, $key, $seen); 918: } elsif(ref($v) eq 'ARRAY') { 919: for my $i (0..$#{$v}) { 920: if(!ref($v->[$i]) && defined($v->[$i]) && $v->[$i] =~ /^ENC\[/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

921: $v->[$i] = $self->_decrypt_enc_value($v->[$i], $key); 922: } 923: } 924: } elsif(!ref($v) && defined($v) && $v =~ /^ENC\[/) { 925: $href->{$k} = $self->_decrypt_enc_value($v, $key); 926: } 927: } 928: } 929: 930: # Decrypt a single ENC[AES256GCM,<base64url>] token. 931: # Returns the plaintext string or croaks on failure. 932: sub _decrypt_enc_value 933: { 934 → 936 → 945 934: my ($self, $token, $key) = @_; 935: 936: unless($token =~ /^

Mutants (Total: 1, Killed: 1, Survived: 0)

937: ENC\[ 938: ([A-Za-z0-9]+) # algorithm name (e.g. AES256GCM) 939: , 940: ([A-Za-z0-9_-]+) # base64url payload (RFC 4648 §5 alphabet) 941: \] 942: $/x) { 943: Carp::croak(ref($self) . ": malformed ENC token: $token"); 944: } 945 → 947 → 954 945: my ($algo, $b64) = ($1, $2); 946: 947: unless(lc($algo) eq 'aes256gcm') {

Mutants (Total: 1, Killed: 1, Survived: 0)

948: Carp::croak(ref($self) . ": unsupported encryption algorithm '$algo' in ENC token"); 949: } 950: 951: # Decode and validate payload length before requiring CryptX: a structurally 952: # invalid token should be rejected regardless of whether the crypto library 953: # is installed. MIME::Base64 is a core module and always available. 954 → 957 → 961 954: require MIME::Base64; 955: my $raw = MIME::Base64::decode_base64url($b64); 956: 957: if(length($raw) < $_ENC_PAYLOAD_MIN) {

Mutants (Total: 4, Killed: 4, Survived: 0)

958: Carp::croak(ref($self) . ": ENC token payload is too short to be valid"); 959: } 960: 961 → 961 → 965 961: unless($self->_load_driver('Crypt::AuthEnc::GCM')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

962: Carp::croak(ref($self) . ': CryptX (Crypt::AuthEnc::GCM) is required to decrypt ENC[] values; install it with: cpanm CryptX'); 963: } 964: 965 → 974 → 978 965: my $nonce = substr($raw, 0, $_AES_NONCE_SIZE); 966: my $tag = substr($raw, -$_AES_TAG_SIZE); 967: my $ct = substr($raw, $_AES_NONCE_SIZE, length($raw) - $_ENC_PAYLOAD_MIN); 968: 969: my $gcm = Crypt::AuthEnc::GCM->new('AES', $key); 970: $gcm->iv_add($nonce); 971: my $pt = $gcm->decrypt_add($ct); 972: 973: my $ok = eval { $gcm->decrypt_done($tag) }; 974: unless($ok && !$@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

975: Carp::croak(ref($self) . ": decryption failed (wrong key or tampered data)"); 976: } 977: 978: return $pt;

Mutants (Total: 2, Killed: 2, Survived: 0)

979: } 980: 981: # Apply the validators hash supplied as a constructor option. 982: # Each key is a dotted config key; each value is a coderef, regex, type string, or spec hashref. 983: sub _run_validators 984: { 985 → 987 → 0 985: my ($self, $validators) = @_; 986: 987: for my $key (sort keys %{$validators}) { 988: my $spec = $validators->{$key}; 989: my $value = $self->get($key); 990: 991: if(ref($spec) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

992: unless($spec->($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

993: Carp::croak(ref($self) . ": validation failed for '$key': custom validator returned false"); 994: } 995: } elsif(ref($spec) eq 'Regexp') { 996: unless(defined($value) && $value =~ $spec) {

Mutants (Total: 1, Killed: 1, Survived: 0)

997: Carp::croak(ref($self) . ": '$key' value " . 998: (defined($value) ? "'$value'" : '(undef)') . " does not match required pattern"); 999: } 1000: } elsif(ref($spec) eq 'HASH') { 1001: $self->_validate_value_spec($key, $value, $spec); 1002: } elsif(!ref($spec)) { 1003: _validate_type($key, $value, $spec); 1004: } else { 1005: Carp::croak(ref($self) . ": invalid validator for '$key': must be a type string, regex, coderef, or hashref"); 1006: } 1007: } 1008: } 1009: 1010: # Apply a hashref spec (type / pattern / min / max / required) to a single value. 1011: sub _validate_value_spec 1012: { 1013 → 1015 → 1018 1013: my ($self, $key, $value, $spec) = @_; 1014: 1015: if($spec->{'required'} && (!$self->exists($key) || !defined($value))) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1016: Carp::croak(ref($self) . ": required key '$key' is missing or undefined"); 1017: } 1018 → 1018 → 1021 1018: if(my $type = $spec->{'type'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1019: _validate_type($key, $value, $type); 1020: } 1021 → 1021 → 1027 1021: if(my $pattern = $spec->{'pattern'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1022: unless(defined($value) && $value =~ $pattern) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1023: Carp::croak(ref($self) . ": '$key' value " . 1024: (defined($value) ? "'$value'" : '(undef)') . " does not match required pattern"); 1025: } 1026: } 1027 → 1027 → 1031 1027: if(defined(my $min = $spec->{'min'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1028: Carp::croak(ref($self) . ": '$key' value '$value' is less than minimum $min") 1029: if defined($value) && $value < $min;

Mutants (Total: 3, Killed: 3, Survived: 0)

1030: } 1031 → 1031 → 0 1031: if(defined(my $max = $spec->{'max'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1032: Carp::croak(ref($self) . ": '$key' value '$value' exceeds maximum $max") 1033: if defined($value) && $value > $max;

Mutants (Total: 3, Killed: 3, Survived: 0)

1034: } 1035: } 1036: 1037: # Validate a single value against a named type. 1038: # Not a method — takes (key, value, type_string). 1039: sub _validate_type 1040: { 1041 → 1044 → 0 1041: my ($key, $value, $type) = @_; 1042: my $ltype = lc($type); 1043: 1044: if($ltype eq 'integer') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1045: Carp::croak("Config::Abstraction: '$key' must be an integer (got " . 1046: (defined $value ? "'$value'" : 'undef') . ')') 1047: unless defined($value) && $value =~ /^-?[0-9]+$/; 1048: } elsif($ltype eq 'number' || $ltype eq 'float') { 1049: Carp::croak("Config::Abstraction: '$key' must be a number (got " . 1050: (defined $value ? "'$value'" : 'undef') . ')') 1051: unless defined($value) && Scalar::Util::looks_like_number($value); 1052: } elsif($ltype eq 'boolean') { 1053: Carp::croak("Config::Abstraction: '$key' must be a boolean (0/1/true/false/yes/no), got " . 1054: (defined $value ? "'$value'" : 'undef')) 1055: unless defined($value) && $value =~ /^(?:1|0|true|false|yes|no)$/i; 1056: } elsif($ltype eq 'string') { 1057: Carp::croak("Config::Abstraction: '$key' must be a defined string (got undef)") 1058: unless defined($value) && !ref($value); 1059: } elsif($ltype eq 'array') { 1060: Carp::croak("Config::Abstraction: '$key' must be an array reference") 1061: unless ref($value) eq 'ARRAY'; 1062: } elsif($ltype eq 'hash') { 1063: Carp::croak("Config::Abstraction: '$key' must be a hash reference") 1064: unless ref($value) eq 'HASH'; 1065: } else { 1066: Carp::croak("Config::Abstraction: unknown type '$type' in validators for '$key'"); 1067: } 1068: } 1069: 1070: # Invoke Config::Checker with the merged config and caller-supplied prototype. 1071: # prototype may be a YAML string or a hashref. 1072: sub _run_checker 1073: { 1074 → 1076 → 1083 1074: my ($self, $prototype) = @_; 1075: 1076: unless($self->_load_driver('Config::Checker')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1077: Carp::carp(ref($self) . ': Config::Checker not available; skipping checker validation'); 1078: return; 1079: } 1080: 1081: # config_checker_source() returns Perl source text (a sub { ... } literal) 1082: # that must be eval'd so it can import into our namespace. 1083: my $checker = eval Config::Checker::config_checker_source(); ## no critic (ProhibitStringyEval) 1084: Carp::croak(ref($self) . ": failed to compile Config::Checker: $@") if $@; 1085: 1086: eval { $checker->($self->{'config'}, $prototype) }; 1087: Carp::croak(ref($self) . ": checker validation failed: $@") if $@; 1088: } 1089: 1090: # Determine if a value is a plain, unblessed, non-reference scalar 1091: # safe to use in regex/string operations. 1092: # Args: value to test 1093: # Returns: 1 if plain scalar, 0 otherwise 1094: sub _is_plain_scalar 1095: { 1096: my $val = $_[0]; 1097: 1098: return 0 if !defined($val);

Mutants (Total: 2, Killed: 2, Survived: 0)

1099: return 0 if Scalar::Util::blessed($val);

Mutants (Total: 2, Killed: 2, Survived: 0)

1100: return 0 if ref($val);

Mutants (Total: 2, Killed: 2, Survived: 0)

1101: return 1;

Mutants (Total: 2, Killed: 2, Survived: 0)

1102: } 1103: 1104: # Recursively flatten a nested hashref to dotted keys (always uses '.' as separator). 1105: # Skips the meta-key 'config_path'. Used by explain_sources() and source tracking. 1106: # $seen guards against circular references (e.g. those possible when 1107: # Hash::Merge::set_clone_behavior(0) is active). 1108: # 1109: # _flatten_into writes into a caller-supplied hashref accumulator so that 1110: # each leaf key is written exactly once -- O(N) total writes vs the previous 1111: # O(N^2) pattern of %flat = (%flat, _flatten_keys(...)) at each recursion level. 1112: sub _flatten_into 1113: { 1114 → 1118 → 0 1114: my ($acc, $hash, $prefix, $seen) = @_; 1115: return unless ref($hash) eq 'HASH'; 1116: my $addr = Scalar::Util::refaddr($hash); 1117: return if $seen->{$addr}++; 1118: for my $k (keys %$hash) { 1119: next if $k eq 'config_path'; 1120: my $full = length($prefix) ? "$prefix.$k" : $k; 1121: if(ref($hash->{$k}) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1122: _flatten_into($acc, $hash->{$k}, $full, $seen); 1123: } else { 1124: $acc->{$full} = $hash->{$k}; 1125: } 1126: } 1127: } 1128: 1129: sub _flatten_keys 1130: { 1131: my ($hash, $prefix, $seen) = @_; 1132: my %flat; 1133: _flatten_into(\%flat, $hash, $prefix // '', $seen // {}); 1134: return %flat;

Mutants (Total: 2, Killed: 2, Survived: 0)

1135: } 1136: 1137: sub _load_config 1138: { 1139 → 1139 → 1143 1139: if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1140: Carp::croak('Illegal Operation: This method can only be called by a subclass'); 1141: } 1142: 1143 → 1154 → 1168 1143: my $self = shift; 1144: 1145: # Disable Hash::Merge cloning for the duration of this method. 1146: # Storable::dclone (used when clone=1) cannot handle coderefs or blessed 1147: # objects that may appear in the 'data' argument; sharing references is 1148: # safe here because %merged is a fresh, method-local accumulator. 1149: my $saved_clone = Hash::Merge::get_clone_behavior(); 1150: Hash::Merge::set_clone_behavior(0); 1151: 1152: my %merged; 1153: 1154: if($self->{'data'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1155: # The data argument given to 'new' contains defaults that this routine will override 1156: if(ref($self->{'data'}) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1157: %merged = %{$self->{'data'}}; 1158: push @{$self->{'_source_records'}}, { 1159: type => 'data', 1160: label => 'constructor data argument', 1161: flat_data => { _flatten_keys($self->{'data'}) }, 1162: }; 1163: } else { 1164: Carp::carp(ref($self) . ': data argument must be a hashref; ignoring non-hashref value'); 1165: } 1166: } 1167: 1168 → 1169 → 1173 1168: my $logger = $self->{'logger'}; 1169: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1170: $logger->trace(ref($self), ' ', __LINE__, ': Entered _load_config'); 1171: } 1172: 1173 → 1183 → 1192 1173: my $environment = $self->_get_environment(); 1174: my @_formats = qw(yaml yml json xml ini toml); 1175: my @_file_list = ( 1176: (map { "base.$_" } @_formats), 1177: ($environment ? (map { "base.$environment.$_" } @_formats) : ()), 1178: (map { "local.$_" } @_formats), 1179: ($environment ? (map { "local.$environment.$_" } @_formats) : ()), 1180: ); 1181: 1182: my @dirs = @{$self->{'config_dirs'}}; 1183: if($self->{'config_file'} && (scalar(@dirs) > 1)) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1184: if(File::Spec->file_name_is_absolute($self->{'config_file'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1185: # Handle absolute paths 1186: @dirs = (''); 1187: } else { 1188: # Look in the current directory 1189: push @dirs, File::Spec->curdir(); 1190: } 1191: } 1192 → 1192 → 1536 1192: for my $dir (@dirs) { 1193: next if(!defined($dir)); 1194: 1195: # Newcastle Connection: /../hostname/path means read from a remote host. 1196: # /../ is unreachable on any real filesystem, so this is unambiguous. 1197: # When the hostname resolves to the local machine (localhost, 127.0.0.1, 1198: # ::1, or the system hostname) the /../host/ wrapper is unwrapped and the 1199: # enclosed path is processed through the normal local pipeline instead. 1200: my $effective_dir = $dir; 1201: if(my ($host, $remote_dir) = $self->_parse_remote_dir($dir)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1202: if($self->_is_local_host($host)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1203: $effective_dir = $remote_dir; 1204: } else { 1205: $self->_load_remote_dir($host, $remote_dir, \%merged, \@_file_list); 1206: next; 1207: } 1208: } 1209: 1210: if(length($effective_dir) && !-d $effective_dir) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1211: next; 1212: } 1213: 1214: for my $file (@_file_list) { 1215: my $path = File::Spec->catfile($effective_dir, $file); 1216: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1217: $logger->debug(ref($self), ' ', __LINE__, ": Looking for configuration $path"); 1218: } 1219: next unless -f $path; 1220: next unless -r $path; 1221: 1222: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1223: $logger->debug(ref($self), ' ', __LINE__, ": Loading data from $path"); 1224: } 1225: 1226: my $data; 1227: # Only load config modules when they are needed 1228: if ($file =~ /\.ya?ml$/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1229: $self->_load_driver('YAML::XS', ['LoadFile']); 1230: $data = eval { LoadFile($path) }; 1231: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1232: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1233: $logger->notice("Failed to load YAML from $path: $@"); 1234: } else { 1235: Carp::carp("Failed to load YAML from $path: $@"); 1236: } 1237: next; 1238: } 1239: $data = $self->_sanitize_yaml_values($data) if defined($data) && ref($data); 1240: } elsif ($file =~ /\.json$/) { 1241: $data = eval { decode_json(read_file($path)) }; 1242: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1243: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1244: $logger->notice("Failed to load JSON from $path: $@"); 1245: } else { 1246: Carp::carp("Failed to load JSON from $path: $@"); 1247: } 1248: next; 1249: } 1250: } elsif($file =~ /\.xml$/) { 1251: my $rc; 1252: if($self->_load_driver('XML::Simple', ['XMLin'])) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1253: my $xml_raw = eval { read_file($path) }; 1254: if(defined($xml_raw) && $xml_raw =~ /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\b/i) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1255: Carp::carp(ref($self) . ": skipping $path: XML external entity declarations not permitted"); 1256: } elsif(defined($xml_raw)) { 1257: eval { $rc = XMLin(\$xml_raw, ForceArray => 0, KeyAttr => []) }; 1258: if($@) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1259: if($logger) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1260: $logger->notice("Failed to load XML from $path: $@"); 1261: } else { 1262: Carp::carp("Failed to load XML from $path: $@"); 1263: } 1264: undef $rc; 1265: } elsif($rc) { 1266: $data = $rc; 1267: } 1268: } 1269: } 1270: if((!defined($rc)) && $self->_load_driver('XML::PP')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1271: my $xml_pp = XML::PP->new(); 1272: $data = read_file($path); 1273: if(my $tree = $xml_pp->parse(\$data)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1274: if($data = $xml_pp->collapse_structure($tree)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1275: $self->{'type'} = 'XML'; 1276: if($data->{'config'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1277: $data = $data->{'config'}; 1278: } 1279: } 1280: } 1281: } 1282: } elsif ($file =~ /\.ini$/) { 1283: $self->_load_driver('Config::IniFiles'); 1284: if(my $ini = Config::IniFiles->new(-file => $path)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1285: $data = { map { 1286: my $section = $_; 1287: $section => { map { $_ => $ini->val($section, $_) } $ini->Parameters($section) } 1288: } $ini->Sections() }; 1289: } else { 1290: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1291: $logger->notice("Failed to load INI from $path: $@"); 1292: } else { 1293: Carp::carp("Failed to load INI from $path: $@"); 1294: } 1295: } 1296: } elsif ($file =~ /\.toml$/) { 1297: if($self->_load_driver('TOML::Tiny', ['from_toml'])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1298: # scalar() forces read_file into scalar context; without it 1299: # read_file returns a list of lines and from_toml (no prototype) 1300: # only sees the first line. 1301: my ($toml_data, $toml_err) = from_toml(scalar(read_file($path))); 1302: if($toml_err) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1303: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1304: $logger->notice("Failed to load TOML from $path: $toml_err"); 1305: } else { 1306: Carp::carp("Failed to load TOML from $path: $toml_err"); 1307: } 1308: } elsif(ref($toml_data) eq 'HASH' && scalar(keys %$toml_data)) { 1309: $data = $toml_data; 1310: } 1311: } 1312: } 1313: if($data) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1314: if(!ref($data)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1315: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1316: $logger->debug(ref($self), ' ', __LINE__, ": ignoring data from $path ($data)"); 1317: } 1318: next; 1319: } 1320: if(ref($data) ne 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1321: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1322: $logger->debug(ref($self), ' ', __LINE__, ": ignoring data from $path (not a hashref)"); 1323: } 1324: next; 1325: } 1326: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1327: $logger->debug(ref($self), ' ', __LINE__, ": Loaded data from $path"); 1328: } 1329: push @{$self->{'_source_records'}}, { 1330: type => 'file', 1331: label => $path, 1332: flat_data => { _flatten_keys($data) }, 1333: }; 1334: %merged = %{ merge( $data, \%merged ) }; 1335: push @{$merged{'config_path'}}, $path; 1336: } 1337: } 1338: 1339: # Put $self->{config_file} through all parsers, ignoring all errors, then merge that in 1340: if(!$self->{'script_name'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1341: require File::Basename && File::Basename->import() unless File::Basename->can('basename'); 1342: 1343: # Determine script name 1344: $self->{'script_name'} = File::Basename::basename($ENV{'SCRIPT_NAME'} || $0); 1345: } 1346: 1347: my $script_name = $self->{'script_name'}; 1348: for my $config_file ('default', $script_name, "$script_name.cfg", "$script_name.conf", "$script_name.config", $self->{'config_file'}, @{$self->{'config_files'}}) { 1349: next unless defined($config_file); 1350: # Note that loading $script_name in the current directory could mean loading the script as its own config. 1351: # This test is not foolproof, buyer beware 1352: next if(($config_file eq $script_name) && ((length($effective_dir) == 0) || ($effective_dir eq File::Spec->curdir())));

Mutants (Total: 1, Killed: 0, Survived: 1)
1353: my $path = length($effective_dir) ? File::Spec->catfile($effective_dir, $config_file) : $config_file; 1354: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1355: $logger->debug(ref($self), ' ', __LINE__, ": Looking for configuration $path"); 1356: } 1357: if((-f $path) && (-r $path)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1358: my $data = read_file($path); 1359: my $raw_content = $data; 1360: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1361: $logger->debug(ref($self), ' ', __LINE__, ": Loading data from $path"); 1362: } 1363: eval { 1364: if(($data =~ /^\s*<\?xml/) || ($data =~ /<\/[^>]+>/)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1365: if($self->_load_driver('XML::Simple', ['XMLin'])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1366: if($data =~ /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\b/i) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1367: Carp::carp(ref($self) . ": skipping $path: XML external entity declarations not permitted"); 1368: undef $data; 1369: } elsif(my $xml_data = XMLin(\$data, ForceArray => 0, KeyAttr => [])) { 1370: $data = $xml_data; 1371: $self->{'type'} = 'XML'; 1372: } 1373: } elsif($self->_load_driver('XML::PP')) { 1374: my $xml_pp = XML::PP->new(); 1375: if(my $tree = $xml_pp->parse(\$data)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1376: if($data = $xml_pp->collapse_structure($tree)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1377: $self->{'type'} = 'XML'; 1378: if($data->{'config'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1379: $data = $data->{'config'}; 1380: } 1381: } 1382: } 1383: } 1384: } elsif($data =~ / 1385: \{ # opening brace 1386: .+ # key material; dot matches newlines because of the s modifier 1387: : # colon separator 1388: . # one value character 1389: \} # closing brace 1390: /xs) { 1391: $self->_load_driver('JSON::Parse'); 1392: # CPanel::JSON is very noisy, so be careful before attempting to use it 1393: my $is_json; 1394: eval { $is_json = JSON::Parse::parse_json($data) }; 1395: if($is_json) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1396: eval { $data = decode_json($data) }; 1397: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1398: undef $data; 1399: } 1400: } else { 1401: undef $data; 1402: } 1403: if($data) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1404: $self->{'type'} = 'JSON'; 1405: } 1406: } else { 1407: undef $data; 1408: } 1409: if(!$data && $raw_content !~ /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\b/i) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1410: $self->_load_driver('YAML::XS', ['LoadFile']); 1411: if((eval { $data = LoadFile($path) }) && (ref($data) eq 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1412: $data = $self->_sanitize_yaml_values($data); 1413: # Could be colon file, could be YAML, whichever it is break the configuration fields 1414: # foreach my($k, $v) (%{$data}) { 1415: foreach my $k (keys %{$data}) { 1416: my $v = $data->{$k}; 1417: if(!defined($v)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1418: # e.g. a simple line 1419: # foo: 1420: # with nothing under it 1421: $data->{$k} = undef; 1422: next; 1423: } 1424: # Do not inspect or modify coderefs, blessed objects, or any reference 1425: next unless _is_plain_scalar($v); 1426: 1427: next if($v =~ /^"[^"]+"$/); # Single-quoted field — skip comma-splitting 1428: if($v =~ /,/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1429: my @vals = split(/\s*,\s*/, $v); 1430: delete $data->{$k}; 1431: foreach my $val (@vals) { 1432: if($val =~ /([^=]+)=(.+)/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1433: $data->{$k}{$1} = $2; 1434: } else { 1435: $data->{$k}{$val} = 1; 1436: } 1437: } 1438: } 1439: } 1440: if($data) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1441: $self->{'type'} = 'YAML'; 1442: } 1443: } 1444: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1445: if($self->_load_driver('TOML::Tiny', ['from_toml'])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1446: # scalar() forces read_file into scalar context; without it 1447: # read_file returns a list of lines and from_toml (no prototype) 1448: # only sees the first line. 1449: my ($toml_data, $toml_err) = eval { from_toml(scalar(read_file($path))) }; 1450: if(!$toml_err && ref($toml_data) eq 'HASH' && scalar(keys %$toml_data)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1451: $data = $toml_data; 1452: $self->{'type'} = 'TOML'; 1453: } 1454: } 1455: } 1456: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1457: $self->_load_driver('Config::IniFiles'); 1458: if(my $ini = Config::IniFiles->new(-file => $path)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1459: $data = { map { 1460: my $section = $_; 1461: $section => { map { $_ => $ini->val($section, $_) } $ini->Parameters($section) } 1462: } $ini->Sections() }; 1463: if($data) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1464: $self->{'type'} = 'INI'; 1465: } 1466: } 1467: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1468: # Maybe XML without the leading XML header 1469: if($self->_load_driver('XML::Simple', ['XMLin'])) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1470: my $xml_raw = eval { read_file($path) }; 1471: if(defined($xml_raw) && $xml_raw !~ /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\b/i) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1472: eval { $data = XMLin(\$xml_raw, ForceArray => 0, KeyAttr => []) }; 1473: } 1474: } 1475: if((!$data) || (ref($data) ne 'HASH')) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1476: if($self->_load_driver('Config::Abstract')) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1477: # Handle RT#164587 1478: open my $oldSTDERR, '>&STDERR'; 1479: close STDERR; 1480: eval { $data = Config::Abstract->new($path) }; 1481: my $err = $@; 1482: open STDERR, '>&', $oldSTDERR; 1483: if($err) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1484: undef $data; 1485: } elsif($data) { 1486: $data = $data->get_all_settings(); 1487: if(scalar(keys %{$data}) == 0) {
Mutants (Total: 2, Killed: 0, Survived: 2)
1488: undef $data; 1489: } 1490: } 1491: $self->{'type'} = 'Perl'; 1492: } 1493: } 1494: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1495: $self->_load_driver('Config::Auto'); 1496: my $ca = Config::Auto->new(source => $path); 1497: if($data = $ca->parse()) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1498: $self->{'type'} = $ca->format(); 1499: } 1500: } 1501: } 1502: } 1503: } 1504: }; 1505: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1506: if($@) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1507: $logger->warn(ref($self), ' ', __LINE__, ": $@"); 1508: undef $data; 1509: } else { 1510: $logger->debug(ref($self), ' ', __LINE__, ': Loaded data from', $self->{'type'}, "file $path"); 1511: } 1512: } 1513: if($data && ref($data) eq 'HASH') {
Mutants (Total: 1, Killed: 0, Survived: 1)
1514: push @{$self->{'_source_records'}}, { 1515: type => 'file', 1516: label => $path, 1517: flat_data => { _flatten_keys($data) }, 1518: }; 1519: } 1520: if(scalar(keys %merged)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1521: if($data) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1522: %merged = %{ merge($data, \%merged) }; 1523: } 1524: } elsif($data && (ref($data) eq 'HASH')) { 1525: %merged = %{$data}; 1526: } elsif((!$@) && $logger) { 1527: $logger->debug(ref($self), ' ', __LINE__, ': No configuration file loaded'); 1528: } 1529: 1530: push @{$merged{'config_path'}}, $path; 1531: } 1532: } 1533: } 1534: 1535: # Merge ENV vars 1536 → 1540 → 1564 1536: my $prefix = $self->{env_prefix}; 1537: $prefix =~ s/__$//; 1538: $prefix =~ s/_$//; 1539: $prefix =~ s/::$//; 1540: for my $key (keys %ENV) { 1541: next unless $key =~ /^\Q$self->{env_prefix}\E(.*)$/i; 1542: my $path = lc($1); 1543: if($path =~ /__/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1544: my @parts = split /__/, $path; 1545: my $ref = \%merged; 1546: $ref = ($ref->{$_} //= {}) for @parts[0..$#parts-1]; 1547: $ref->{ $parts[-1] } = $ENV{$key}; 1548: push @{$self->{'_source_records'}}, { 1549: type => 'env', 1550: label => $key, 1551: flat_data => { join('.', @parts) => $ENV{$key} }, 1552: }; 1553: } else { 1554: $merged{$prefix}->{$path} = $ENV{$key}; 1555: push @{$self->{'_source_records'}}, { 1556: type => 'env', 1557: label => $key, 1558: flat_data => { "$prefix.$path" => $ENV{$key} }, 1559: }; 1560: } 1561: } 1562: 1563: # Merge command line options 1564 → 1564 → 1585 1564: foreach my $arg(@ARGV) { 1565: next unless($arg =~ /=/); 1566: my ($key, $value) = split(/=/, $arg, 2); 1567: next unless $key =~ /^--\Q$self->{env_prefix}\E(.*)$/; 1568: 1569: my $path = lc($1); 1570: my @parts = split(/__/, $path); 1571: if(scalar(@parts) > 0) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1572: my $ref = \%merged; 1573: if(scalar(@parts) > 1) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1574: $ref = ($ref->{$_} //= {}) for @parts[0..$#parts-1]; 1575: } 1576: $ref->{$parts[-1]} = $value; 1577: push @{$self->{'_source_records'}}, { 1578: type => 'argv', 1579: label => $arg, 1580: flat_data => { join('.', @parts) => $value }, 1581: }; 1582: } 1583: } 1584: 1585 → 1585 → 1592 1585: if($self->{'flatten'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1586: $self->_load_driver('Hash::Flatten', ['flatten']); 1587: } else { 1588: $self->_load_driver('Hash::Flatten', ['unflatten']); 1589: } 1590: # $self->{config} = $self->{flatten} ? flatten(\%merged) : unflatten(\%merged); 1591: # Don't unflatten because of RT#166761 1592 → 1596 → 0 1592: $self->{config} = $self->{flatten} ? flatten(\%merged) : \%merged; 1593: 1594: Hash::Merge::set_clone_behavior($saved_clone); 1595: 1596: if(my $enc_key = $self->_get_encryption_key()) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1597: $self->_decrypt_config_values($self->{'config'}, $enc_key); 1598: } 1599: } 1600: 1601: =head2 get(key) 1602: 1603: Retrieve a configuration value using dotted key notation (e.g., 1604: C<'database.user'>). Returns C<undef> if the key doesn't exist or if 1605: C<key> is C<undef>. 1606: 1607: =head3 EXAMPLE 1608: 1609: my $cfg = Config::Abstraction->new( 1610: data => { database => { host => 'localhost', port => 5432 } }, 1611: config_dirs => [], 1612: ); 1613: 1614: my $host = $cfg->get('database.host'); # 'localhost' 1615: my $port = $cfg->get('database.port'); # 5432 1616: my $miss = $cfg->get('database.user'); # undef -- key absent 1617: 1618: =head3 API SPECIFICATION 1619: 1620: =head4 Input 1621: 1622: key -- SCALAR -- dotted key path (e.g. 'database.host'). 1623: C<undef> is allowed and returns C<undef> silently. 1624: 1625: =head4 Output 1626: 1627: SCALAR or reference -- the value stored under C<key>, or C<undef> if absent. 1628: 1629: =head3 MESSAGES 1630: 1631: (none) -- missing keys return undef, no warning is raised. 1632: 1633: =head3 PSEUDOCODE 1634: 1635: if key is undef: return undef 1636: call _ensure_loaded 1637: if flatten mode: return config[key] (direct lookup) 1638: parts = split sep_char from key 1639: ref = config hashref 1640: for each part: 1641: if ref is not a HASH: return undef 1642: if part not in ref: return undef 1643: ref = ref[part] 1644: return ref 1645: 1646: =cut 1647: 1648: sub get 1649: { 1650 → 1656 → 1659 1650: my ($self, $key) = @_; 1651: 1652: return undef unless defined $key;

Mutants (Total: 2, Killed: 2, Survived: 0)

1653: 1654: $self->_ensure_loaded(); 1655: 1656: if($self->{flatten}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1657: return $self->{config}{$key};

Mutants (Total: 2, Killed: 2, Survived: 0)

1658: } 1659 → 1660 → 1665 1659: my $ref = $self->{'config'}; 1660: for my $part (split $self->{'_sep_re'}, $key) { 1661: return undef unless ref $ref eq 'HASH';

Mutants (Total: 2, Killed: 2, Survived: 0)

1662: return unless exists $ref->{$part}; 1663: $ref = $ref->{$part}; 1664: } 1665 → 1665 → 1684 1665: if((defined($ref) && (ref($ref) eq 'HASH') && !$self->{'no_fixate'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1666: if($self->_load_data_reuse()) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1667: if(ref($ref) eq 'HASH') {
Mutants (Total: 1, Killed: 0, Survived: 1)
1668: if(!tied %$ref) {
Mutants (Total: 1, Killed: 0, Survived: 1)
1669: # Pass the hashref directly (not dereferenced) so fixate receives 1670: # a named scalar it can make read-only without flattening the hash 1671: # FIXME: 1672: # What works on MacOS doesn't work 1673: # on Linux and vice versa. 1674: # Something is wrong. 1675: # Data::Reuse::fixate(%{$ref}) if scalar(keys %{$ref}); 1676: # Data::Reuse::fixate($ref) if scalar(keys %{$ref}); 1677: } 1678: } elsif(ref($ref) eq 'ARRAY') { 1679: # RT#171980 1680: # Data::Reuse::fixate(@{$ref}); 1681: } 1682: } 1683: } 1684: return $ref;

Mutants (Total: 2, Killed: 2, Survived: 0)

1685: } 1686: 1687: sub _load_data_reuse 1688: { 1689 → 1702 → 1707 1689: my $self = $_[0]; 1690: 1691: # Skip fixation entirely if caller has opted out 1692: return 0 if($self->{'no_fixate'});

Mutants (Total: 2, Killed: 2, Survived: 0)

1693: 1694: # Return cached result to avoid repeated require attempts 1695: return 1 if($self->{reuse_loaded});

Mutants (Total: 2, Killed: 2, Survived: 0)

1696: return 0 if($self->{reuse_failed});

Mutants (Total: 2, Killed: 2, Survived: 0)

1697: 1698: eval { 1699: require Data::Reuse; 1700: Data::Reuse->import(); 1701: }; 1702: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1703: # Cache the failure so we do not attempt to load again 1704: $self->{reuse_failed} = 1; 1705: return 0;

Mutants (Total: 2, Killed: 2, Survived: 0)

1706: } 1707: $self->{reuse_loaded} = 1; 1708: return 1;

Mutants (Total: 2, Killed: 0, Survived: 2)
1709: } 1710: 1711: =head2 exists(key) 1712: 1713: Test whether a configuration key is present, using dotted key notation 1714: (e.g., C<'database.user'>). Returns C<1> when the key exists (even if its 1715: value is C<undef>), C<0> otherwise. Returns C<0> when C<key> is C<undef>. 1716: 1717: =head3 EXAMPLE 1718: 1719: my $cfg = Config::Abstraction->new( 1720: data => { timeout => undef, retries => 3 }, 1721: config_dirs => [], 1722: ); 1723: 1724: $cfg->exists('timeout'); # 1 -- key present even though value is undef 1725: $cfg->exists('retries'); # 1 1726: $cfg->exists('missing'); # 0 1727: $cfg->exists(undef); # 0 1728: 1729: =head3 API SPECIFICATION 1730: 1731: =head4 Input 1732: 1733: key -- SCALAR -- dotted key path. C<undef> returns C<0>. 1734: 1735: =head4 Output 1736: 1737: boolean 1738: 1739: =head3 MESSAGES 1740: 1741: (none) 1742: 1743: =cut 1744: 1745: sub exists 1746: { 1747 → 1753 → 1756 1747: my ($self, $key) = @_; 1748: 1749: return 0 unless defined $key;

Mutants (Total: 2, Killed: 2, Survived: 0)

1750: 1751: $self->_ensure_loaded(); 1752: 1753: if($self->{flatten}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1754: return exists($self->{config}{$key}) ? 1 : 0;

Mutants (Total: 2, Killed: 2, Survived: 0)

1755: } 1756 → 1757 → 1762 1756: my $ref = $self->{'config'}; 1757: for my $part (split $self->{'_sep_re'}, $key) { 1758: return 0 unless ref $ref eq 'HASH';

Mutants (Total: 2, Killed: 2, Survived: 0)

1759: return 0 if(!exists($ref->{$part}));

Mutants (Total: 2, Killed: 2, Survived: 0)

1760: $ref = $ref->{$part}; 1761: } 1762: return 1;

Mutants (Total: 2, Killed: 2, Survived: 0)

1763: } 1764: 1765: =head2 all() 1766: 1767: Returns the entire merged configuration as a hashref, or C<undef> when no 1768: configuration data was found. When C<flatten =E<gt> 1> was given to the 1769: constructor the keys are dotted strings (e.g. C<'database.host'>); otherwise 1770: the hash is nested. 1771: 1772: The special key C<config_path> within the returned hashref is an arrayref 1773: listing every file that was loaded, in load order. 1774: 1775: =head3 EXAMPLE 1776: 1777: my $cfg = Config::Abstraction->new( 1778: data => { host => 'localhost', port => 5432 }, 1779: config_dirs => [], 1780: ); 1781: 1782: my $all = $cfg->all(); 1783: # { host => 'localhost', port => 5432, config_path => [] } 1784: 1785: =head3 API SPECIFICATION 1786: 1787: =head4 Input 1788: 1789: (none) 1790: 1791: =head4 Output 1792: 1793: HASHREF -- the full merged config (includes C<config_path> key). 1794: undef -- when the merged config is empty and no data was supplied. 1795: 1796: =head3 MESSAGES 1797: 1798: (none) -- returns undef silently when empty. 1799: 1800: =cut 1801: 1802: sub all 1803: { 1804: my $self = shift; 1805: 1806: $self->_ensure_loaded(); 1807: 1808: return if(!$self->{config}); 1809: 1810: # This is good for debugging, but not much more and it breaks inheritance, so disabled 1811: # if($self->_load_data_reuse()) { 1812: # Data::Reuse::fixate($self->{config}); 1813: # } 1814: 1815: return(scalar(keys %{$self->{'config'}})) ? $self->{'config'} : undef;

Mutants (Total: 2, Killed: 2, Survived: 0)

1816: } 1817: 1818: =head2 explain_sources() 1819: 1820: Returns a hashref describing where each configuration key came from and in 1821: what order the sources that set it were applied. 1822: 1823: Each key of the returned hashref is a dotted key name (e.g. C<'database.user'>). 1824: The corresponding value is a hashref with two fields: 1825: 1826: =over 4 1827: 1828: =item * C<value> 1829: 1830: The final merged value for that key after all sources have been applied. 1831: 1832: =item * C<sources> 1833: 1834: An arrayref of hashrefs, ordered from lowest to highest precedence (i.e. the 1835: last element is always the winning source). Each entry has: 1836: 1837: =over 4 1838: 1839: =item * C<type> -- one of C<'data'>, C<'file'>, C<'env'>, or C<'argv'> 1840: 1841: =item * C<label> -- a human-readable identifier: a file path, an environment 1842: variable name (e.g. C<'APP_DATABASE__USER'>), a CLI argument string, or 1843: C<'constructor data argument'> 1844: 1845: =item * C<value> -- what this source set the key to (may differ from C<value> 1846: at the top level if a later source overrode it) 1847: 1848: =back 1849: 1850: =back 1851: 1852: Keys set by exactly one source have a single-element C<sources> list. 1853: Keys whose value was never overridden will show the same C<value> in both the 1854: top-level field and the sole C<sources> entry. 1855: 1856: =head3 USAGE EXAMPLE 1857: 1858: use Config::Abstraction; 1859: 1860: local $ENV{APP_HOST} = 'prod.example.com'; 1861: 1862: my $cfg = Config::Abstraction->new( 1863: data => { host => 'localhost', port => 5432 }, 1864: config_dirs => ['/etc/myapp'], 1865: ); 1866: 1867: use Data::Dumper; 1868: print Dumper( $cfg->explain_sources() ); 1869: # { 1870: # 'host' => { 1871: # value => 'prod.example.com', 1872: # sources => [ 1873: # { type => 'data', label => 'constructor data argument', value => 'localhost' }, 1874: # { type => 'env', label => 'APP_HOST', value => 'prod.example.com' }, 1875: # ], 1876: # }, 1877: # 'port' => { 1878: # value => 5432, 1879: # sources => [ 1880: # { type => 'data', label => 'constructor data argument', value => 5432 }, 1881: # ], 1882: # }, 1883: # } 1884: 1885: =head3 API SPECIFICATION 1886: 1887: =head4 Input 1888: 1889: None (instance method; takes no arguments beyond C<$self>). 1890: 1891: =head4 Output 1892: 1893: HASHREF where each key is a dotted config key and each value is: 1894: 1895: { 1896: value => SCALAR, 1897: sources => [ 1898: { 1899: type => 'data'|'file'|'env'|'argv', 1900: label => SCALAR, 1901: value => SCALAR, 1902: }, 1903: ... 1904: ], 1905: } 1906: 1907: =cut 1908: 1909: sub explain_sources 1910: { 1911 → 1918 → 1934 1911: my $self = shift; 1912: 1913: $self->_ensure_loaded(); 1914: 1915: my %final_flat = _flatten_keys($self->{'config'}); 1916: my %result; 1917: 1918: for my $key (keys %final_flat) { 1919: my @key_sources; 1920: for my $layer (@{$self->{'_source_records'} // []}) { 1921: if(exists $layer->{'flat_data'}{$key}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1922: push @key_sources, { 1923: type => $layer->{'type'}, 1924: label => $layer->{'label'}, 1925: value => $layer->{'flat_data'}{$key}, 1926: }; 1927: } 1928: } 1929: $result{$key} = { 1930: value => $final_flat{$key}, 1931: sources => \@key_sources, 1932: }; 1933: } 1934: return \%result;

Mutants (Total: 2, Killed: 2, Survived: 0)

1935: } 1936: 1937: # --------------------------------------------------------------------------- 1938: # _value_from_type -- scan _source_records for the last record of the given 1939: # type that provided a value for $key. Source records always use '.' as the 1940: # key separator regardless of sep_char, so we normalise $key before the lookup. 1941: # 1942: # Returns ($found, $value): $found is true when the source type contributed to 1943: # the key, letting callers distinguish "source set key to undef" from "source 1944: # never set it". 1945: # --------------------------------------------------------------------------- 1946: sub _value_from_type 1947: { 1948 → 1958 → 1965 1948: my ($self, $type, $key) = @_; 1949: 1950: return (0, undef) unless defined $key; 1951: 1952: $self->_ensure_loaded(); 1953: 1954: my $sep = $self->{'sep_char'}; 1955: my $flat_key = ($sep eq '.') ? $key : join('.', split /\Q$sep\E/, $key); 1956: 1957: my ($found, $val); 1958: for my $layer (@{$self->{'_source_records'} // []}) { 1959: next unless $layer->{'type'} eq $type; 1960: if(exists $layer->{'flat_data'}{$flat_key}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1961: $found = 1; 1962: $val = $layer->{'flat_data'}{$flat_key}; 1963: } 1964: } 1965: return ($found, $val); 1966: } 1967: 1968: =head2 prefer_env(key) 1969: 1970: Return the value that an environment variable provided for C<key>, bypassing 1971: any later sources (e.g. CLI arguments) that may have overridden it. 1972: Falls back to the normal merged value from C<get(key)> when no environment 1973: variable contributed to C<key>. 1974: 1975: =head3 EXAMPLE 1976: 1977: local $ENV{APP_DATABASE__HOST} = 'env-host'; 1978: my $host = $cfg->prefer_env('database.host'); 1979: # Returns 'env-host' even if --APP_DATABASE__HOST=cli-host was also passed. 1980: 1981: =head3 API SPECIFICATION 1982: 1983: =head4 Input 1984: 1985: key -- SCALAR -- dotted key path. 1986: 1987: =head4 Output 1988: 1989: SCALAR -- the env-layer value, or C<get(key)> when no env var set it. 1990: 1991: =cut 1992: 1993: sub prefer_env 1994: { 1995: my ($self, $key) = @_; 1996: my ($found, $val) = $self->_value_from_type('env', $key); 1997: return $found ? $val : $self->get($key);

Mutants (Total: 2, Killed: 2, Survived: 0)

1998: } 1999: 2000: =head2 prefer_file(key) 2001: 2002: Return the value that a configuration file provided for C<key>, bypassing 2003: environment variables and CLI arguments that may have overridden it. 2004: Falls back to the normal merged value from C<get(key)> when no file 2005: contributed to C<key>. 2006: 2007: =head3 EXAMPLE 2008: 2009: my $host = $cfg->prefer_file('database.host'); 2010: # Returns the file-sourced value even if APP_DATABASE__HOST is set. 2011: 2012: =head3 API SPECIFICATION 2013: 2014: =head4 Input 2015: 2016: key -- SCALAR -- dotted key path. 2017: 2018: =head4 Output 2019: 2020: SCALAR -- the file-layer value, or C<get(key)> when no file set it. 2021: 2022: =cut 2023: 2024: sub prefer_file 2025: { 2026: my ($self, $key) = @_; 2027: my ($found, $val) = $self->_value_from_type('file', $key); 2028: return $found ? $val : $self->get($key);

Mutants (Total: 2, Killed: 2, Survived: 0)

2029: } 2030: 2031: =head2 prefer_data(key) 2032: 2033: Return the value that the C<data> constructor argument provided for C<key>, 2034: bypassing files, environment variables, and CLI arguments that may have 2035: overridden it. 2036: Falls back to the normal merged value from C<get(key)> when C<data> did not 2037: contribute to C<key>. 2038: 2039: =head3 EXAMPLE 2040: 2041: my $cfg = Config::Abstraction->new( 2042: data => { timeout => 30 }, 2043: config_dirs => ['/etc/myapp'], 2044: ); 2045: my $t = $cfg->prefer_data('timeout'); # always 30, regardless of files/env 2046: 2047: =head3 API SPECIFICATION 2048: 2049: =head4 Input 2050: 2051: key -- SCALAR -- dotted key path. 2052: 2053: =head4 Output 2054: 2055: SCALAR -- the data-layer value, or C<get(key)> when C<data> did not set it. 2056: 2057: =cut 2058: 2059: sub prefer_data 2060: { 2061: my ($self, $key) = @_; 2062: my ($found, $val) = $self->_value_from_type('data', $key); 2063: return $found ? $val : $self->get($key);

Mutants (Total: 2, Killed: 2, Survived: 0)

2064: } 2065: 2066: =head2 prefer_argv(key) 2067: 2068: Return the value that a CLI argument provided for C<key>. 2069: Falls back to the normal merged value from C<get(key)> when no CLI argument 2070: contributed to C<key>. 2071: 2072: Because CLI arguments are the highest-precedence source, this method is 2073: primarily useful for writing self-documenting code or for detecting whether 2074: a key was explicitly supplied on the command line. 2075: 2076: =head3 EXAMPLE 2077: 2078: my $level = $cfg->prefer_argv('log.level'); 2079: # Equivalent to $cfg->get('log.level') unless you specifically need to 2080: # confirm the value came from @ARGV. 2081: 2082: =head3 API SPECIFICATION 2083: 2084: =head4 Input 2085: 2086: key -- SCALAR -- dotted key path. 2087: 2088: =head4 Output 2089: 2090: SCALAR -- the argv-layer value, or C<get(key)> when no CLI arg set it. 2091: 2092: =cut 2093: 2094: sub prefer_argv 2095: { 2096: my ($self, $key) = @_; 2097: my ($found, $val) = $self->_value_from_type('argv', $key); 2098: return $found ? $val : $self->get($key);

Mutants (Total: 2, Killed: 2, Survived: 0)

2099: } 2100: 2101: =head2 encrypt_value($plaintext) 2102: 2103: Encrypt a plaintext string using the configured AES-256-GCM key and return an 2104: C<ENC[AES256GCM,...]> token suitable for storing in a configuration file. 2105: 2106: Each call generates a fresh random nonce, so the same plaintext produces a 2107: different token every time. The token is authenticated (GCM tag); any 2108: modification causes decryption to croak. 2109: 2110: Requires L<CryptX> (C<Crypt::AuthEnc::GCM>, C<Crypt::PRNG>). 2111: 2112: =head3 EXAMPLE 2113: 2114: # Generate a token to paste into base.yaml: 2115: my $cfg = Config::Abstraction->new( 2116: encryption_key => $hex_key, 2117: config_dirs => [], 2118: lazy => 1, 2119: ); 2120: my $token = $cfg->encrypt_value('s3cr3t_password'); 2121: # ENC[AES256GCM,QkJCQkJCQkJCQkJCO6Gfb0o5jwqB1R...] 2122: 2123: # Or use config-dump from the command line: 2124: # config-dump --encrypt-value 's3cr3t_password' --encryption-key $KEY 2125: 2126: =head3 API SPECIFICATION 2127: 2128: =head4 Input 2129: 2130: plaintext -- SCALAR -- the string to encrypt. May be empty. 2131: 2132: =head4 Output 2133: 2134: SCALAR -- the ENC[AES256GCM,...] token (always a printable ASCII string). 2135: 2136: =head3 MESSAGES 2137: 2138: "<class>: no encryption key configured ..." -- croak when no key is available. 2139: "<class>: CryptX (Crypt::AuthEnc::GCM) is required ..." -- croak when CryptX absent. 2140: 2141: =head3 PSEUDOCODE 2142: 2143: call _ensure_loaded 2144: key = _get_encryption_key() -- croak if undef 2145: load Crypt::AuthEnc::GCM and Crypt::PRNG -- croak if absent 2146: nonce = 12 random bytes 2147: gcm = new GCM('AES', key) 2148: gcm.iv_add(nonce) 2149: ciphertext = gcm.encrypt_add(plaintext) 2150: tag = gcm.encrypt_done() 2151: return "ENC[AES256GCM," + base64url(nonce + ciphertext + tag) + "]" 2152: 2153: =cut 2154: 2155: sub encrypt_value 2156: { 2157 → 2164 → 2167 2157: my ($self, $plaintext) = @_; 2158: 2159: $self->_ensure_loaded(); 2160: 2161: my $key = $self->_get_encryption_key() 2162: or Carp::croak(ref($self) . ': no encryption key configured (set encryption_key, encryption_key_file, or ENCRYPTION_KEY env var)'); 2163: 2164: unless($self->_load_driver('Crypt::AuthEnc::GCM')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2165: Carp::croak(ref($self) . ': CryptX (Crypt::AuthEnc::GCM) is required; install it with: cpanm CryptX'); 2166: } 2167 → 2167 → 2171 2167: unless($self->_load_driver('Crypt::PRNG', ['random_bytes'])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2168: Carp::croak(ref($self) . ': CryptX (Crypt::PRNG) is required; install it with: cpanm CryptX'); 2169: } 2170: 2171: require MIME::Base64; 2172: 2173: my $nonce = random_bytes($_AES_NONCE_SIZE); 2174: my $gcm = Crypt::AuthEnc::GCM->new('AES', $key); 2175: $gcm->iv_add($nonce); 2176: my $ct = $gcm->encrypt_add($plaintext); 2177: my $tag = $gcm->encrypt_done(); 2178: 2179: return 'ENC[AES256GCM,' . MIME::Base64::encode_base64url($nonce . $ct . $tag, '') . ']';

Mutants (Total: 2, Killed: 2, Survived: 0)

2180: } 2181: 2182: =head2 merge_defaults 2183: 2184: Merge the configuration hash into the given hash. 2185: 2186: package MyPackage; 2187: use Params::Get; 2188: use Config::Abstraction; 2189: 2190: sub new 2191: { 2192: my $class = shift; 2193: 2194: my $params = Params::Get::get_params(undef, \@_) || {}; 2195: 2196: if(my $config = Config::Abstraction->new(env_prefix => "${class}::")) { 2197: $params = $config->merge_defaults(defaults => $params, merge => 1, section => $class); 2198: } 2199: 2200: return bless $params, $class; 2201: } 2202: 2203: Options: 2204: 2205: =over 4 2206: 2207: =item * merge 2208: 2209: Usually, 2210: what's in the object will overwrite what's in the defaults hash, 2211: if given, 2212: the result will be a combination of the hashes. 2213: 2214: =item * section 2215: 2216: Merge in that section from the configuration file. 2217: 2218: =item * deep 2219: 2220: Try harder to merge all configurations from the global section of the configuration file. 2221: 2222: =back 2223: 2224: =cut 2225: 2226: sub merge_defaults 2227: { 2228 → 2250 → 2258 2228: my $self = shift; 2229: my $config = $self->all(); 2230: 2231: return $config if(scalar(@_) == 0);

Mutants (Total: 3, Killed: 3, Survived: 0)

2232: 2233: my $params = Params::Get::get_params('defaults', @_); 2234: my $defaults = $params->{'defaults'}; 2235: return $config if(!defined($defaults));

Mutants (Total: 2, Killed: 2, Survived: 0)

2236: my $section = $params->{'section'}; 2237: 2238: # Work on a shallow copy so we never mutate $self->{'config'} directly. 2239: # Without this, 'delete $config->{global}' below would permanently alter 2240: # the live internal hash on every call. 2241: my %config_copy = %{$config}; 2242: $config = \%config_copy; 2243: 2244: # Save and restore Hash::Merge's clone behaviour so we don't leak the 2245: # no-clone global state into unrelated merge() calls (e.g. inside 2246: # _load_config when a new object is constructed after this method runs). 2247: my $saved_clone = Hash::Merge::get_clone_behavior(); 2248: Hash::Merge::set_clone_behavior(0); 2249: 2250: if(exists $config->{'global'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2251: if($params->{'deep'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2252: $defaults = merge($config->{'global'}, $defaults); 2253: } else { 2254: $defaults = { %{$defaults}, %{$config->{'global'}} }; 2255: } 2256: delete $config->{'global'}; 2257: } 2258 → 2258 → 2261 2258: if($section && exists $config->{$section}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2259: $config = $config->{$section}; 2260: } 2261 → 2262 → 2267 2261: my $result; 2262: if($params->{'merge'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2263: $result = merge($config, $defaults); 2264: } else { 2265: $result = { %{$defaults}, %{$config} }; 2266: } 2267: Hash::Merge::set_clone_behavior($saved_clone); 2268: return $result;

Mutants (Total: 2, Killed: 2, Survived: 0)

2269: } 2270: 2271: # Helper routine to load a driver. 2272: # NOTE: Log::Abstraction must NOT be loaded via this method - it is 2273: # bootstrapped directly in new() to avoid a circular initialisation 2274: # dependency where _load_driver would attempt to log via an as-yet 2275: # uninitialised logger. 2276: sub _load_driver 2277: { 2278 → 2284 → 2291 2278: my($self, $driver, $imports) = @_; 2279: 2280: return 1 if($self->{'loaded'}{$driver});

Mutants (Total: 2, Killed: 2, Survived: 0)

2281: return 0 if($self->{'failed'}{$driver});

Mutants (Total: 2, Killed: 2, Survived: 0)

2282: 2283: eval "require $driver"; 2284: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2285: if(my $logger = $self->{'logger'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2286: $logger->warn(ref($self), ": $driver failed to load: $@"); 2287: } 2288: $self->{'failed'}{$driver} = 1; 2289: return; 2290: } 2291: $driver->import(@{ $imports // [] }); 2292: $self->{'loaded'}{$driver} = 1; 2293: return 1;

Mutants (Total: 2, Killed: 2, Survived: 0)

2294: } 2295: 2296: =head2 Remote configuration directories (Newcastle Connection) 2297: 2298: Any entry in C<config_dirs> whose path begins with C</../> is treated as a 2299: remote specification rather than a local directory: 2300: 2301: /../hostname/path/to/dir 2302: 2303: The hostname and directory are extracted, and the same standard files searched 2304: locally (C<base.yaml>, C<local.yaml>, C<base.json>, etc.) are fetched from 2305: the remote machine via L<File::Slurp::Remote> over SSH. 2306: 2307: When the hostname resolves to the local machine -- C<localhost>, C<127.0.0.1>, 2308: C<::1>, or the value returned by C<Sys::Hostname::hostname()> (checked as 2309: both fully-qualified and short form, case-insensitively) -- the C</../host/> 2310: wrapper is silently unwrapped and the enclosed path is processed through the 2311: normal local file pipeline instead. No SSH connection is made. This means a 2312: configuration written for a shared remote host degrades gracefully when run on 2313: that host itself: 2314: 2315: # On any other machine: fetches /etc/myapp over SSH 2316: # On cfg-server itself: reads /etc/myapp from disk directly 2317: config_dirs => ['/../cfg-server/etc/myapp'] 2318: 2319: Remote directories participate in the normal merge pipeline and can be freely 2320: mixed with local ones: 2321: 2322: my $cfg = Config::Abstraction->new( 2323: config_dirs => [ 2324: '/etc/myapp', # local 2325: '/../deploy@cfg-server/etc/myapp', # remote via SSH (local on cfg-server) 2326: ], 2327: ); 2328: 2329: SSH authentication is handled by the system SSH client. 2330: No extra constructor options are required; use your SSH agent or 2331: C<~/.ssh/config> for host-specific settings. 2332: 2333: L<File::Slurp::Remote> must be installed for remote directories to work. 2334: If it is absent the directory is silently skipped and a warning is emitted. 2335: 2336: =head3 Why C</../> (the Newcastle Connection convention) 2337: 2338: Several syntaxes were considered for marking a C<config_dirs> entry as remote. 2339: Each alternative was rejected for a concrete reason: 2340: 2341: =over 4 2342: 2343: =item C<hostname/path> -- ambiguous 2344: 2345: Indistinguishable from a relative local directory named C<hostname>. 2346: The module cannot tell at parse time whether C<myserver/etc> is a two-level 2347: local path or a remote specification. 2348: 2349: =item C<hostname:/path> -- collides with Windows drive letters 2350: 2351: The C<X:\> drive-letter convention on Windows uses exactly the same 2352: C<letter:> prefix. A single-letter hostname would be misidentified as a 2353: drive, and path-normalisation code on Windows would mangle it. 2354: 2355: =item C<file://hostname/path> -- wrong semantics 2356: 2357: RFC 8089 defines C<file://> as a reference to a B<local> file. 2358: C<file:///etc/passwd> (three slashes, empty host) is the canonical local form; 2359: C<file://hostname/path> is reserved for the host component but is explicitly 2360: discouraged for general use and is not understood by most tooling as meaning 2361: SSH. 2362: 2363: =item C<ssh://hostname/path> -- requires URI parsing 2364: 2365: Introduces a dependency on URI parsing (or a bespoke prefix check) and 2366: implies a specific transport. C<File::Slurp::Remote> already abstracts 2367: the transport; encoding C<ssh://> in the path would be misleading if a future 2368: version of that module supports other transports such as C<rsync://>. 2369: 2370: =item C</../hostname/path> -- the Newcastle Connection 2371: 2372: The Newcastle Connection (Brownbridge, Dion, Elsworth, 1982) is a distributed 2373: Unix convention in which the token C</../> at the start of a pathname means 2374: "leave the local namespace and enter the named remote host". 2375: It works because C</../> B<cannot exist> as a real filesystem path: 2376: C<..> from the root directory resolves back to the root on every POSIX system, 2377: so C</../> always denotes the root itself, never a child of the root. 2378: No pathname-normalisation step, C<chdir>, or filesystem traversal will ever 2379: produce a path that legitimately begins with C</../>, which means the prefix 2380: is a permanent, collision-free sentinel that requires only a regex to detect. 2381: 2382: =back 2383: 2384: The Newcastle Connection prefix is therefore the only choice that is: 2385: 2386: =over 4 2387: 2388: =item * unambiguous on all platforms (POSIX and Windows) 2389: 2390: =item * impossible to produce accidentally from a real local path 2391: 2392: =item * detectable with a single C<m{^\Q/../\E}> regex, no URI parser needed 2393: 2394: =item * transport-neutral (the hostname is passed to whatever remote driver is installed) 2395: 2396: =back 2397: 2398: =head2 AUTOLOAD 2399: 2400: This module supports dynamic access to configuration keys via AUTOLOAD. 2401: Nested keys are accessible using the separator, 2402: so C<$config-E<gt>database_user()> resolves to C<< $config->{database}->{user} >>, 2403: when C<sep_char> is set to '_'. 2404: 2405: $config = Config::Abstraction->new( 2406: data => { 2407: database => { 2408: user => 'alice', 2409: pass => 'secret' 2410: }, 2411: log_level => 'debug' 2412: }, 2413: flatten => 1, 2414: sep_char => '_' 2415: ); 2416: 2417: my $user = $config->database_user(); # returns 'alice' 2418: 2419: # or 2420: $user = $config->database()->{'user'}; # returns 'alice' 2421: 2422: # Attempting to call a nonexistent key 2423: my $foo = $config->nonexistent_key(); # dies with error 2424: 2425: =cut 2426: 2427: # --------------------------------------------------------------------------- 2428: # _parse_remote_dir -- detect Newcastle Connection style paths. 2429: # 2430: # Returns ($host, $dir) when $dir begins with /../, empty list otherwise. 2431: # The /../ prefix is syntactically impossible for a real local path so no 2432: # real directory entry is ever misidentified. 2433: # --------------------------------------------------------------------------- 2434: sub _parse_remote_dir 2435: { 2436: my ($self, $dir) = @_; 2437: 2438: return unless defined($dir); 2439: return unless $dir =~ m{^\Q/../\E([^/]+)(/.+)?$}; 2440: return ($1, $2 // '/'); 2441: } 2442: 2443: # --------------------------------------------------------------------------- 2444: # _is_local_host -- true when $host refers to the machine running this code. 2445: # 2446: # Strips any user@ prefix first, then checks the four common ways a caller 2447: # might spell "here": the loopback name, the two loopback addresses, and the 2448: # system hostname (both fully-qualified and short). Comparison is 2449: # case-insensitive because hostnames are case-insensitive by RFC 1034. 2450: # --------------------------------------------------------------------------- 2451: sub _is_local_host 2452: { 2453 → 2465 → 2470 2453: my ($self, $host) = @_; 2454: 2455: return 0 unless defined($host) && length($host);

Mutants (Total: 2, Killed: 2, Survived: 0)

2456: 2457: (my $bare = $host) =~ s/^[^@]+@//; # strip optional user@ prefix 2458: 2459: return 1 if lc($bare) eq 'localhost';

Mutants (Total: 2, Killed: 2, Survived: 0)

2460: return 1 if $bare eq '127.0.0.1';

Mutants (Total: 2, Killed: 2, Survived: 0)

2461: return 1 if $bare eq '::1';

Mutants (Total: 2, Killed: 2, Survived: 0)

2462: 2463: # Cache the resolved hostname on the object so Sys::Hostname::hostname() 2464: # (a syscall) is only made once per Config::Abstraction instance. 2465: unless(defined $self->{'_cached_hostname'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2466: require Sys::Hostname; 2467: $self->{'_cached_hostname'} = lc(Sys::Hostname::hostname()); 2468: ($self->{'_cached_short_hostname'} = $self->{'_cached_hostname'}) =~ s/\..*$//; 2469: } 2470: return 1 if lc($bare) eq $self->{'_cached_hostname'};

Mutants (Total: 2, Killed: 2, Survived: 0)

2471: return 1 if lc($bare) eq $self->{'_cached_short_hostname'};

Mutants (Total: 2, Killed: 2, Survived: 0)

2472: 2473: return 0;

Mutants (Total: 2, Killed: 2, Survived: 0)

2474: } 2475: 2476: # --------------------------------------------------------------------------- 2477: # _load_remote_dir -- fetch and merge standard config files from one remote 2478: # host/directory pair. 2479: # 2480: # Silently skips files that do not exist; warns (or logs) on parse errors. 2481: # Merges into %$merged_ref using the same Hash::Merge LEFT_PRECEDENT strategy 2482: # as the local pipeline, so remote values override earlier local values. 2483: # --------------------------------------------------------------------------- 2484: sub _load_remote_dir 2485: { 2486 → 2486 → 2490 2486: if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2487: Carp::croak('Illegal Operation: This method can only be called by a subclass'); 2488: } 2489: 2490 → 2493 → 2501 2490: my ($self, $host, $remote_dir, $merged_ref, $file_list) = @_; 2491: my $logger = $self->{'logger'}; 2492: 2493: unless($self->_load_driver('File::Slurp::Remote')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2494: my $msg = ref($self) . ": File::Slurp::Remote required for /../$host$remote_dir but is not installed"; 2495: $logger ? $logger->warn($msg) : Carp::carp($msg); 2496: return; 2497: } 2498: 2499: # Use the same file list as the local pipeline (includes TOML and env-specific tiers). 2500: # Fall back to the legacy list if none was passed (e.g. from very old callers). 2501 → 2505 → 0 2501: my @files = $file_list ? @{$file_list} 2502: : qw(base.yaml base.yml base.json base.xml base.ini base.toml 2503: local.yaml local.yml local.json local.xml local.ini local.toml); 2504: 2505: for my $file (@files) { 2506: my $remote_path = "/../$host$remote_dir/$file"; 2507: 2508: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2509: $logger->debug(ref($self), ' ', __LINE__, ": Looking for remote config $remote_path"); 2510: } 2511: 2512: my $raw = $self->_slurp_remote($host, "$remote_dir/$file"); 2513: next unless defined($raw); 2514: 2515: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2516: $logger->debug(ref($self), ' ', __LINE__, ": Loading remote config $remote_path"); 2517: } 2518: 2519: my $data = $self->_parse_config_string($raw, $file, $remote_path); 2520: next unless defined($data); 2521: 2522: if(ref($data) ne 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

2523: my $msg = ref($self) . ": remote $remote_path did not yield a hash; skipping"; 2524: $logger ? $logger->warn($msg) : Carp::carp($msg); 2525: next; 2526: } 2527: 2528: push @{$self->{'_source_records'}}, { 2529: type => 'file', 2530: label => $remote_path, 2531: flat_data => { _flatten_keys($data) }, 2532: }; 2533: %{$merged_ref} = %{ merge($data, $merged_ref) }; 2534: push @{$merged_ref->{'config_path'}}, $remote_path; 2535: } 2536: } 2537: 2538: # --------------------------------------------------------------------------- 2539: # _slurp_remote -- read a single file from a remote host via SSH. 2540: # 2541: # Wraps File::Slurp::Remote::read_file; returns the raw string on success 2542: # or undef on any error (connection refused, file absent, permission denied). 2543: # Errors are logged at debug level so missing files are not noisy. 2544: # --------------------------------------------------------------------------- 2545: sub _slurp_remote 2546: { 2547 → 2554 → 2560 2547: my ($self, $host, $path) = @_; 2548: my $logger = $self->{'logger'}; 2549: 2550: # NOTE: verify the calling convention of your installed File::Slurp::Remote. 2551: # Common forms: read_file("$host:$path") or read_file($host, $path). 2552: my $content = eval { File::Slurp::Remote::read_file($host, $path) }; 2553: 2554: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2555: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2556: $logger->debug(ref($self), ' ', __LINE__, ": Could not read $path from $host: $@"); 2557: } 2558: return undef;

Mutants (Total: 2, Killed: 2, Survived: 0)

2559: } 2560: return $content;

Mutants (Total: 2, Killed: 2, Survived: 0)

2561: } 2562: 2563: # --------------------------------------------------------------------------- 2564: # _parse_config_string -- parse a raw config string by extension. 2565: # 2566: # Mirrors the format-detection logic used for local files. INI content is 2567: # written to a File::Temp scratch file because Config::IniFiles requires a 2568: # real filesystem path; the temp file is removed as soon as parsing returns. 2569: # 2570: # Returns a hashref on success, undef on failure. 2571: # --------------------------------------------------------------------------- 2572: sub _parse_config_string 2573: { 2574 → 2574 → 2578 2574: if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2575: Carp::croak('Illegal Operation: This method can only be called by a subclass'); 2576: } 2577: 2578 → 2619 → 2626 2578: my ($self, $raw, $filename, $label) = @_; 2579: my $logger = $self->{'logger'}; 2580: my $data; 2581: 2582: eval { 2583: if($filename =~ /\.ya?ml$/i) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2584: $self->_load_driver('YAML::XS', ['Load']); 2585: $data = YAML::XS::Load($raw); 2586: $data = $self->_sanitize_yaml_values($data) if defined($data) && ref($data); 2587: 2588: } elsif($filename =~ /\.json$/i) { 2589: $data = decode_json($raw); 2590: 2591: } elsif($filename =~ /\.xml$/i) { 2592: if($self->_load_driver('XML::Simple', ['XMLin'])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2593: if($raw !~ /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\b/i) {

Mutants (Total: 1, Killed: 0, Survived: 1)
2594: $data = XMLin(\$raw, ForceArray => 0, KeyAttr => []); 2595: } 2596: } elsif($self->_load_driver('XML::PP')) { 2597: my $pp = XML::PP->new(); 2598: if(my $tree = $pp->parse(\$raw)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2599: $data = $pp->collapse_structure($tree); 2600: $data = $data->{'config'} if ($data && $data->{'config'}); 2601: } 2602: } 2603: 2604: } elsif($filename =~ /\.ini$/i) { 2605: $self->_load_driver('Config::IniFiles'); 2606: require File::Temp; 2607: my $tmp = File::Temp->new(SUFFIX => '.ini', UNLINK => 1); 2608: print {$tmp} $raw; 2609: $tmp->flush(); 2610: if(my $ini = Config::IniFiles->new(-file => $tmp->filename())) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2611: $data = { map { 2612: my $section = $_; 2613: $section => { map { $_ => $ini->val($section, $_) } $ini->Parameters($section) } 2614: } $ini->Sections() }; 2615: } 2616: } 2617: }; 2618: 2619: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2620: my $err = $@; 2621: my $msg = ref($self) . ": Failed to parse $label: $err"; 2622: $logger ? $logger->warn($msg) : Carp::carp($msg); 2623: return undef;

Mutants (Total: 2, Killed: 2, Survived: 0)

2624: } 2625: 2626: return $data;

Mutants (Total: 2, Killed: 2, Survived: 0)

2627: } 2628: 2629: sub AUTOLOAD 2630: { 2631 → 2652 → 2660 2631: our $AUTOLOAD; 2632: 2633: my $self = shift; 2634: my $key = $AUTOLOAD; 2635: 2636: $key =~ s/.*:://; # remove package name 2637: return if $key eq 'DESTROY'; 2638: 2639: $self->_ensure_loaded(); 2640: 2641: # my $val = $self->get($key); 2642: # return $val if(defined($val)); 2643: 2644: # Always use the merged config, not the raw pre-merge $self->{data}. 2645: # Using $self->{data} here would bypass file and env overrides entirely. 2646: my $data = $self->{'config'}; 2647: my $sep = $self->{'sep_char'}; 2648: 2649: # When flattening is ON, Hash::Flatten stores keys with '.' as separator. 2650: # Convert the sep_char-separated AUTOLOAD key to the dotted form first, 2651: # then fall back to the raw key in case sep_char happens to be '.'. 2652: if($self->{flatten}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2653: my $dot_key = ($sep ne '.') ? do { (my $k = $key) =~ s/\Q$sep\E/./g; $k } : $key; 2654: return $data->{$dot_key} if exists $data->{$dot_key};

Mutants (Total: 2, Killed: 2, Survived: 0)

2655: return $data->{$key} if exists $data->{$key};

Mutants (Total: 2, Killed: 2, Survived: 0)

2656: croak "No such config key '$key'"; 2657: } 2658: 2659: # Nested (non-flat) mode: walk the config tree one part at a time. 2660 → 2661 → 2668 2660: my $val = $data; 2661: foreach my $part (split /\Q$sep\E/, $key) { 2662: if((ref($val) eq 'HASH') && (exists $val->{$part})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2663: $val = $val->{$part}; 2664: } else { 2665: croak "No such config key '$key'"; 2666: } 2667: } 2668: return $val;

Mutants (Total: 2, Killed: 2, Survived: 0)

2669: } 2670: 2671: 1; 2672: 2673: =head1 ENCRYPTED VALUES 2674: 2675: Config::Abstraction supports transparent AES-256-GCM encryption of individual 2676: configuration values. This lets you store secrets (passwords, API keys, tokens) 2677: in config files without exposing them as plaintext, even when those files are 2678: committed to version control. 2679: 2680: =head2 Quick start 2681: 2682: B<Step 1 -- generate a key:> 2683: 2684: # 32 random bytes, encoded as 64 hex chars 2685: perl -e 'use Crypt::PRNG qw(random_bytes); use MIME::Base64 qw(encode_base64url); 2686: print encode_base64url(random_bytes(32)), "\n"' 2687: 2688: Store the result in an environment variable or a key file (outside version control): 2689: 2690: export ENCRYPTION_KEY=<the 44-char base64url output> 2691: 2692: B<Step 2 -- encrypt a secret value:> 2693: 2694: perl -MConfig::Abstraction -e ' 2695: my $cfg = Config::Abstraction->new(data => {}); 2696: print $cfg->encrypt_value("my_secret_password"), "\n"; 2697: ' 2698: 2699: This prints something like: 2700: 2701: ENC[AES256GCM,QkJCQkJCQkJCQkJCO6Gfb0o5...] 2702: 2703: B<Step 3 -- paste the token into your config file:> 2704: 2705: # config/base.yaml 2706: database: 2707: host: db.example.com 2708: user: myapp 2709: password: 'ENC[AES256GCM,QkJCQkJCQkJCQkJCO6Gfb0o5...]' 2710: 2711: B<Step 4 -- load and use normally:> 2712: 2713: my $cfg = Config::Abstraction->new(config_dirs => ['config']); 2714: # $cfg->get('database.password') returns 'my_secret_password' -- already decrypted 2715: 2716: =head2 Key configuration 2717: 2718: The encryption key is resolved in this order (first match wins): 2719: 2720: =over 4 2721: 2722: =item 1. C<encryption_key> constructor option (raw bytes, hex, or base64) 2723: 2724: =item 2. C<{env_prefix}ENCRYPTION_KEY> environment variable (default: C<APP_ENCRYPTION_KEY>) 2725: 2726: =item 3. C<ENCRYPTION_KEY> environment variable 2727: 2728: =item 4. File at C<encryption_key_file> constructor option 2729: 2730: =item 5. File at C<{env_prefix}ENCRYPTION_KEY_FILE> environment variable 2731: 2732: =item 6. File at C<ENCRYPTION_KEY_FILE> environment variable 2733: 2734: =back 2735: 2736: The key file should contain the key on its first line in any supported format. 2737: B<Never commit the key to version control.> 2738: 2739: =head2 Token format 2740: 2741: ENC[AES256GCM,<base64url(nonce || ciphertext || tag)>] 2742: 2743: =over 4 2744: 2745: =item * C<AES256GCM> -- AES-256 in GCM mode (authenticated encryption) 2746: 2747: =item * Nonce -- 12 random bytes (fresh per encryption, never reused) 2748: 2749: =item * GCM authentication tag -- 16 bytes; any modification causes decryption to croak 2750: 2751: =item * Base64url encoding -- URL-safe alphabet, no padding ambiguity 2752: 2753: =back 2754: 2755: =head2 Behaviour when no key is configured 2756: 2757: If no key is found, C<ENC[...]> tokens are left as literal strings. This means 2758: the feature is purely opt-in: existing deployments without a key configured are 2759: unaffected. 2760: 2761: =head2 Requirements 2762: 2763: L<CryptX> (C<Crypt::AuthEnc::GCM>, C<Crypt::PRNG>) must be installed: 2764: 2765: cpanm CryptX 2766: 2767: =head1 COMMON PITFALLS 2768: 2769: =head2 1. new() returns undef when no configuration is found 2770: 2771: C<new()> returns C<undef>, not a blessed object, when no configuration data is 2772: found and no C<data> argument was supplied. Every caller must check the return value. 2773: 2774: my $cfg = Config::Abstraction->new(config_dirs => ['/etc/myapp']); 2775: die "No configuration found" unless defined $cfg; 2776: my $host = $cfg->get('database.host'); # safe 2777: 2778: Forgetting the check leads to a cryptic "Can't call method on undef" error later, 2779: with a stack trace that points to C<get()> rather than to the missing config file. 2780: 2781: =head2 2. merge_defaults() requires a named argument, not a bare hashref 2782: 2783: # WRONG -- Params::Get fast-path returns the whole config unchanged 2784: my $merged = $cfg->merge_defaults(\%my_defaults); 2785: 2786: # RIGHT 2787: my $merged = $cfg->merge_defaults(defaults => \%my_defaults); 2788: 2789: With the first form, C<Params::Get> treats the single hashref as the entire 2790: parameter bag and returns the full config without merging anything. 2791: 2792: =head2 3. Shallow merge in merge_defaults() silently drops nested keys from defaults 2793: 2794: Without C<merge =E<gt> 1>, C<merge_defaults()> uses a plain Perl hash merge 2795: (C<{ %defaults, %config }>) at the top level. If the config contains a nested hash 2796: for a key, it entirely replaces the corresponding nested hash in your defaults - any 2797: keys that exist only in the defaults' nested hash are silently discarded. 2798: 2799: my $cfg = Config::Abstraction->new( 2800: data => { db => { host => 'localhost', port => 5432 } }, 2801: config_dirs => [], 2802: ); 2803: 2804: my $merged = $cfg->merge_defaults(defaults => { db => { user => 'guest' } }); 2805: # $merged->{db}{user} is UNDEF -- the whole 'db' hash was replaced by the config's version 2806: 2807: # To combine nested keys from both sides, pass merge => 1: 2808: my $merged = $cfg->merge_defaults(defaults => { db => { user => 'guest' } }, merge => 1); 2809: # $merged->{db}{user} is 'guest', $merged->{db}{host} is 'localhost' 2810: 2811: =head2 4. undef from a higher-priority source permanently wins 2812: 2813: C<Hash::Merge> LEFT_PRECEDENT means that an explicit C<undef> (YAML C<~>) in a 2814: higher-priority source overrides a real value in a lower-priority source, including 2815: when the lower-priority value is defined. 2816: 2817: # base.yaml: timeout: 30 2818: # local.yaml: timeout: ~ 2819: 2820: my $t = $cfg->get('timeout'); # undef -- local.yaml's null wins over base.yaml 2821: 2822: This is intentional: it lets a local config deliberately unset a value. If you 2823: want to detect whether a key was explicitly nulled versus simply absent, use 2824: C<explain_sources()> and inspect the C<sources> list. 2825: 2826: =head2 5. Double underscore vs. single underscore in environment variable names 2827: 2828: Single underscores are part of the key name; double underscores create a nesting level. 2829: 2830: APP_LOG_LEVEL=debug => key 'log_level' (single underscore, flat key) 2831: APP_DATABASE__HOST=db => key 'database.host' (double underscore, nested) 2832: APP_API__RATE_LIMIT=100 => key 'api.rate_limit' (double underscore + single) 2833: 2834: A common mistake is using single underscores expecting nested keys: 2835: 2836: APP_DATABASE_HOST=db # produces key 'database_host', NOT 'database.host' 2837: 2838: =head2 6. Using AUTOLOAD requires sep_char set to '_' 2839: 2840: AUTOLOAD translates method names to config keys using C<sep_char>. The default 2841: C<sep_char> is C<'.'>, but method names cannot contain dots. Set C<sep_char =E<gt> '_'> 2842: to use AUTOLOAD, and be aware that this makes single underscores into hierarchy separators. 2843: 2844: my $cfg = Config::Abstraction->new( 2845: data => { database => { host => 'localhost' } }, 2846: sep_char => '_', 2847: config_dirs => [], 2848: ); 2849: my $host = $cfg->database_host(); # works 2850: my $bad = $cfg->no_such_key(); # dies: No such config key 'no_such_key' 2851: 2852: =head2 7. Absolute config_file paths require an empty or omitted config_dirs 2853: 2854: On Unix, C<File::Spec-E<gt>catfile('/etc', '/absolute/path.yaml')> concatenates the 2855: two strings instead of letting the absolute path take over, producing a wrong path. 2856: When C<config_file> is an absolute path, either omit C<config_dirs> entirely 2857: (the constructor sets it to C<['']> automatically) or pass C<config_dirs =E<gt> ['']> 2858: explicitly. 2859: 2860: # WRONG on Unix -- produces '/etc/etc/myapp/app.yaml' 2861: Config::Abstraction->new( 2862: config_file => '/etc/myapp/app.yaml', 2863: config_dirs => ['/etc'], 2864: ); 2865: 2866: # RIGHT 2867: Config::Abstraction->new( config_file => '/etc/myapp/app.yaml' ); 2868: 2869: =head2 8. Tests must isolate from the developer's real config files 2870: 2871: A call to C<new()> without C<config_dirs> will scan C</etc>, C<~/.conf>, 2872: C<~/.config>, and other default locations and load any C<base.yaml> or 2873: C<local.yaml> it finds there. In a test suite this injects real host 2874: configuration into your test object, causing non-deterministic failures. 2875: 2876: Always pass C<config_dirs =E<gt> []> in tests that use only in-memory C<data>: 2877: 2878: my $cfg = Config::Abstraction->new( 2879: data => { key => 'value' }, 2880: config_dirs => [], # do not scan the filesystem 2881: ); 2882: 2883: =head2 9. lazy => 1 defers errors until the first accessor call 2884: 2885: With C<lazy =E<gt> 1>, C<new()> always returns a blessed object - it cannot return 2886: C<undef> for a missing config, and any schema validation errors surface at the first 2887: C<get()> or C<all()> call rather than at construction time. See the C<lazy> 2888: option documentation in L</new> for the full list of debugging implications. 2889: 2890: =head1 VERSION HISTORY 2891: 2892: Notable changes by release. Full details are in the C<Changes> file. 2893: 2894: =over 4 2895: 2896: =item * B<0.40> (unreleased) 2897: 2898: TOML file support (C<*.toml>) via C<TOML::Tiny>; C<base.toml> and C<local.toml> 2899: are now discovered automatically alongside the YAML/JSON/XML/INI equivalents, 2900: and TOML is also tried in the all-parsers chain for extensionless C<config_file> 2901: entries. 2902: Lazy loading (C<lazy =E<gt> 1> constructor option) defers all source discovery 2903: and file I/O until the first accessor call. 2904: New C<explain_sources()> method returns a per-key audit trail showing every 2905: source that contributed to a value, in precedence order. 2906: New C<prefer_env()>, C<prefer_file()>, C<prefer_data()>, and C<prefer_argv()> 2907: shortcut methods return the value from a specific source layer without re-ordering. 2908: Fixed C<merge_defaults()> permanently mutating the internal config hash 2909: (now works on a shallow copy). 2910: Fixed C<Hash::Merge> clone-behaviour global state leaking between calls 2911: (C<get_clone_behavior()> is now saved and restored, preventing "Can't store CODE 2912: items" crashes when C<data> contains coderefs). 2913: Newcastle Connection remote configuration: C<config_dirs> entries beginning with 2914: C</../hostname/path> are fetched over SSH via L<File::Slurp::Remote>. 2915: Local-host entries (C</../localhost/>, C</../127.0.0.1/>, etc.) are short-circuited 2916: to a plain local read - no SSH connection is made. 2917: 2918: =item * B<0.39> (2026-05-24) 2919: 2920: Disabled C<Data::Reuse::fixate()> - behaviour differed between Linux and macOS 2921: in a way that could not be resolved portably. 2922: 2923: =item * B<0.38> (2026-05-20) 2924: 2925: Fixed corruption of coderefs and blessed objects passed via the C<data> argument 2926: (added C<_is_plain_scalar()> guard in the YAML value-munging loop). 2927: Fixed C<exists()> to return explicit C<0> rather than empty string in flat mode. 2928: Fixed C<Data::Reuse::fixate()> crash in C<get()>. 2929: Fixed circular initialisation crash when the C<logger> option is provided. 2930: Documented the C<defaults> argument to C<new()>. 2931: 2932: =item * B<0.36> (2025-10-15) 2933: 2934: Added C<exists()> method. 2935: Files that fail to parse now emit a warning and are skipped rather than aborting 2936: construction. 2937: 2938: =item * B<0.34> (2025-09-05) 2939: 2940: Added C<bin/config-dump> CLI tool. 2941: Schema validation via C<Params::Validate::Strict> (C<schema> option). 2942: 2943: =item * B<0.25> (2025-05-15) 2944: 2945: Added C<merge_defaults()> C<merge> option (recursive Hash::Merge merge). 2946: 2947: =item * B<0.20> (2025-05-06) 2948: 2949: Added C<merge_defaults()>. 2950: 2951: =item * B<0.19> (2025-05-06) 2952: 2953: The C<data> constructor argument now sets default values that are overridden by files. 2954: 2955: =item * B<0.13> (2025-04-22) 2956: 2957: Added C<data> option to C<new()>. 2958: Added AUTOLOAD support for method-style key access. 2959: Added C<sep_char> option. 2960: 2961: =item * B<0.06> (2025-04-09) 2962: 2963: C<config_path> key in C<all()> output lists the files actually loaded. 2964: Format drivers are now lazy-loaded (only C<require>d when a file of that format is 2965: found). 2966: 2967: =item * B<0.01> (2025-04-07) 2968: 2969: First release. 2970: 2971: =back 2972: 2973: =head1 LIMITATIONS 2974: 2975: =over 4 2976: 2977: =item * B<No separator escaping> 2978: 2979: The separator character (C<sep_char>, default C<.>) cannot be embedded in a key name. 2980: A key that literally contains a dot cannot be accessed via C<get()> when C<sep_char> is 2981: the default. Workaround: set C<sep_char> to a character not present in any key. 2982: 2983: =item * B<Data::Reuse fixation disabled> 2984: 2985: The C<Data::Reuse::fixate()> call inside C<get()> is currently a no-op because the 2986: behaviour of C<Crypt::Storable::dclone> differs between Linux and macOS in a way 2987: that cannot be resolved portably (RT#100461). Hash values returned by C<get()> are 2988: mutable references, not read-only copies. 2989: 2990: =item * B<Windows environment variable case sensitivity> 2991: 2992: On Windows, environment variable names are case-insensitive at the OS level but 2993: case-sensitive in C<%ENV> as seen by Perl. Overriding config keys via environment 2994: variables may silently fail if the case does not match exactly. 2995: 2996: =item * B<AUTOLOAD requires sep_char set to '_'> 2997: 2998: AUTOLOAD method dispatch converts underscores to key separators. If C<sep_char> is 2999: the default C<'.'>, AUTOLOAD cannot reach nested keys because Perl method names cannot 3000: contain dots. 3001: 3002: =item * B<Remote directory TOML support requires File::Slurp::Remote> 3003: 3004: TOML files in Newcastle Connection remote directories are only fetched when the 3005: C<File::Slurp::Remote> module is installed. Without it, remote directories are 3006: skipped entirely regardless of file format. 3007: 3008: =back 3009: 3010: =head1 BUGS 3011: 3012: It should be possible to escape the separator character either with backslashes or quotes. 3013: 3014: Due to the case-insensitive nature of environment variables on Windows, 3015: it may be challenging to override values using environment variables on that platform. 3016: 3017: =head1 REPOSITORY 3018: 3019: L<https://github.com/nigelhorne/Config-Abstraction> 3020: 3021: =head1 SUPPORT 3022: 3023: This module is provided as-is without any warranty. 3024: 3025: Please report any bugs or feature requests to C<bug-config-abstraction at rt.cpan.org>, 3026: or through the web interface at 3027: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Config-Abstraction>. 3028: I will be notified, and then you'll 3029: automatically be notified of progress on your bug as I make changes. 3030: 3031: You can find documentation for this module with the perldoc command. 3032: 3033: perldoc Config::Abstraction 3034: 3035: =head1 SEE ALSO 3036: 3037: =over 4 3038: 3039: =item * L<File::Slurp::Remote> 3040: 3041: Used to fetch configuration from remote hosts when C<config_dirs> contains 3042: Newcastle Connection paths (C</../hostname/path>). 3043: 3044: =item * L<Config::Any> 3045: 3046: =item * L<Config::Auto> 3047: 3048: =item * L<Data::Reuse> 3049: 3050: Used to C<fixate()> elements when installed, unless C<no-fixate> is given 3051: 3052: =item * L<Hash::Merge> 3053: 3054: =item * L<Log::Abstraction> 3055: 3056: =item * L<Test Dashboard|https://nigelhorne.github.io/Config-Abstraction/coverage/> 3057: 3058: =item * Development version on GitHub L<https://github.com/nigelhorne/Config-Abstraction> 3059: 3060: =back 3061: 3062: =head1 AUTHOR 3063: 3064: Nigel Horne, C<< <njh at nigelhorne.com> >> 3065: 3066: =encoding UTF-8 3067: 3068: =head1 FORMAL SPECIFICATION 3069: 3070: =head2 get 3071: 3072: get : Config x Key → Value ∪ {⊥} 3073: get(c, k) ≜ if k = ⊥ then ⊥ 3074: else lookup(c.config, split(c.sep_char, k)) 3075: lookup(h, []) ≜ h 3076: lookup(h, p:rest) ≜ if p ∉ dom(h) then ⊥ 3077: else lookup(h[p], rest) 3078: 3079: =head2 encrypt_value 3080: 3081: encrypt_value : Config x Plaintext → Token 3082: encrypt_value(c, p) ≜ 3083: let k = resolve_key(c) where k ≠ ⊥ 3084: let n ~ Uniform(Bytes^12) -- fresh random nonce 3085: let ct = AES256GCM_enc(k, n, p) 3086: let t = GCM_tag(k, n, p) 3087: in "ENC[AES256GCM," || base64url(n || ct || t) || "]" 3088: 3089: =head2 exists 3090: 3091: exists : Config x Key → {0, 1} 3092: exists(c, k) ≜ if k = ⊥ then 0 3093: else 1 if lookup(c.config, split(c.sep_char, k)) ≠ ⊥ 3094: else 0 3095: 3096: =head2 all 3097: 3098: all : Config → HashRef ∪ {⊥} 3099: all(c) ≜ if |dom(c.config)| = 0 then ⊥ else c.config 3100: 3101: =head1 LICENCE AND COPYRIGHT 3102: 3103: Copyright 2025-2026 Nigel Horne. 3104: 3105: Usage is subject to the GPL2 licence terms. 3106: If you use it, 3107: please let me know. 3108: 3109: =cut 3110: 3111: 1;