#################################################################################################################################### # COMMON INI MODULE #################################################################################################################################### package pgBackRest::Common::Ini; use strict; use warnings FATAL => qw(all); use Carp qw(confess); use English '-no_match_vars'; use Exporter qw(import); our @EXPORT = qw(); use File::Basename qw(dirname); use JSON::PP; use Storable qw(dclone); use pgBackRest::Common::Exception; use pgBackRest::Common::Log; use pgBackRest::Common::String; use pgBackRest::LibC qw(:crypto); use pgBackRest::Version; #################################################################################################################################### # Boolean constants #################################################################################################################################### use constant INI_TRUE => JSON::PP::true; push @EXPORT, qw(INI_TRUE); use constant INI_FALSE => JSON::PP::false; push @EXPORT, qw(INI_FALSE); #################################################################################################################################### # Ini control constants #################################################################################################################################### use constant INI_SECTION_BACKREST => 'backrest'; push @EXPORT, qw(INI_SECTION_BACKREST); use constant INI_KEY_CHECKSUM => 'backrest-checksum'; push @EXPORT, qw(INI_KEY_CHECKSUM); use constant INI_KEY_FORMAT => 'backrest-format'; push @EXPORT, qw(INI_KEY_FORMAT); use constant INI_KEY_VERSION => 'backrest-version'; push @EXPORT, qw(INI_KEY_VERSION); use constant INI_SECTION_CIPHER => 'cipher'; push @EXPORT, qw(INI_SECTION_CIPHER); use constant INI_KEY_CIPHER_PASS => 'cipher-pass'; push @EXPORT, qw(INI_KEY_CIPHER_PASS); #################################################################################################################################### # Ini file copy extension #################################################################################################################################### use constant INI_COPY_EXT => '.copy'; push @EXPORT, qw(INI_COPY_EXT); #################################################################################################################################### # Ini sort orders #################################################################################################################################### use constant INI_SORT_FORWARD => 'forward'; push @EXPORT, qw(INI_SORT_FORWARD); use constant INI_SORT_REVERSE => 'reverse'; push @EXPORT, qw(INI_SORT_REVERSE); use constant INI_SORT_NONE => 'none'; push @EXPORT, qw(INI_SORT_NONE); #################################################################################################################################### # new() #################################################################################################################################### sub new { my $class = shift; # Class name # Create the class hash my $self = {}; bless $self, $class; # Load Storage::Helper module require pgBackRest::Storage::Helper; pgBackRest::Storage::Helper->import(); # Assign function parameters, defaults, and log debug info ( my $strOperation, $self->{strFileName}, my $bLoad, my $strContent, $self->{oStorage}, $self->{iInitFormat}, $self->{strInitVersion}, my $bIgnoreMissing, $self->{strCipherPass}, # Passphrase to read/write the file my $strCipherPassSub, # Passphrase to read/write subsequent files ) = logDebugParam ( __PACKAGE__ . '->new', \@_, {name => 'strFileName', trace => true}, {name => 'bLoad', optional => true, default => true, trace => true}, {name => 'strContent', optional => true, trace => true}, {name => 'oStorage', optional => true, default => storageLocal(), trace => true}, {name => 'iInitFormat', optional => true, default => REPOSITORY_FORMAT, trace => true}, {name => 'strInitVersion', optional => true, default => PROJECT_VERSION, trace => true}, {name => 'bIgnoreMissing', optional => true, default => false, trace => true}, {name => 'strCipherPass', optional => true, trace => true}, {name => 'strCipherPassSub', optional => true, trace => true}, ); if (defined($self->{oStorage}->cipherPassUser()) && !defined($self->{strCipherPass})) { confess &log(ERROR, 'passphrase is required when storage is encrypted', ERROR_CRYPTO); } # Set changed to false $self->{bModified} = false; # Set exists to false $self->{bExists} = false; # Load the file if requested if ($bLoad) { $self->load($bIgnoreMissing); } # Load from a string if provided elsif (defined($strContent)) { $self->{oContent} = iniParse($strContent); $self->headerCheck(); } # Initialize if not loading the file and not loading from string or if a load was attempted and the file does not exist if (!$self->{bExists} && !defined($strContent)) { $self->numericSet(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, $self->{iInitFormat}); $self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion}); # Determine if the passphrase section should be set if (defined($self->{strCipherPass}) && defined($strCipherPassSub)) { $self->set(INI_SECTION_CIPHER, INI_KEY_CIPHER_PASS, undef, $strCipherPassSub); } elsif ((defined($self->{strCipherPass}) && !defined($strCipherPassSub)) || (!defined($self->{strCipherPass}) && defined($strCipherPassSub))) { confess &log(ASSERT, 'a user passphrase and sub passphrase are both required when encrypting'); } } return $self; } #################################################################################################################################### # loadVersion() - load a version (main or copy) of the ini file #################################################################################################################################### sub loadVersion { my $self = shift; my $bCopy = shift; my $bIgnoreError = shift; # Make sure the file encryption setting is valid for the repo if ($self->{oStorage}->encryptionValid($self->{oStorage}->encrypted($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''), {bIgnoreMissing => $bIgnoreError}))) { # Load main my $rstrContent = $self->{oStorage}->get( $self->{oStorage}->openRead($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''), {bIgnoreMissing => $bIgnoreError, strCipherPass => $self->{strCipherPass}})); # If the file exists then attempt to parse it if (defined($rstrContent)) { my $rhContent = iniParse($$rstrContent, {bIgnoreInvalid => $bIgnoreError}); # If the content is valid then check the header if (defined($rhContent)) { $self->{oContent} = $rhContent; # If the header is invalid then undef content if (!$self->headerCheck({bIgnoreInvalid => $bIgnoreError})) { delete($self->{oContent}); } } } } else { confess &log(ERROR, "unable to parse '$self->{strFileName}" . ($bCopy ? INI_COPY_EXT : '') . "'" . "\nHINT: Is or was the repo encrypted?", ERROR_CRYPTO); } return defined($self->{oContent}); } #################################################################################################################################### # load() - load the ini #################################################################################################################################### sub load { my $self = shift; my $bIgnoreMissing = shift; # If main was not loaded then try the copy if (!$self->loadVersion(false, true)) { if (!$self->loadVersion(true, true)) { return if $bIgnoreMissing; confess &log(ERROR, "unable to open $self->{strFileName} or $self->{strFileName}" . INI_COPY_EXT, ERROR_FILE_MISSING); } } $self->{bExists} = true; } #################################################################################################################################### # headerCheck() - check that version and checksum in header are as expected #################################################################################################################################### sub headerCheck { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $bIgnoreInvalid, ) = logDebugParam ( __PACKAGE__ . '->headerCheck', \@_, {name => 'bIgnoreInvalid', optional => true, default => false, trace => true}, ); # Eval so exceptions can be ignored on bIgnoreInvalid my $bValid = true; eval { # Make sure the ini is valid by testing checksum my $strChecksum = $self->get(INI_SECTION_BACKREST, INI_KEY_CHECKSUM, undef, false); my $strTestChecksum = $self->hash(); if (!defined($strChecksum) || $strChecksum ne $strTestChecksum) { confess &log(ERROR, "invalid checksum in '$self->{strFileName}', expected '${strTestChecksum}' but found " . (defined($strChecksum) ? "'${strChecksum}'" : '[undef]'), ERROR_CHECKSUM); } # Make sure that the format is current, otherwise error my $iFormat = $self->get(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, false, 0); if ($iFormat != $self->{iInitFormat}) { confess &log(ERROR, "invalid format in '$self->{strFileName}', expected $self->{iInitFormat} but found ${iFormat}", ERROR_FORMAT); } # Check if the version has changed if (!$self->test(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion})) { $self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion}); } return true; } or do { # Confess the error if it should not be ignored if (!$bIgnoreInvalid) { confess $EVAL_ERROR; } # Return false when errors are ignored $bValid = false; }; # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'bValid', value => $bValid, trace => true} ); } #################################################################################################################################### # iniParse() - parse from standard INI format to a hash. #################################################################################################################################### push @EXPORT, qw(iniParse); sub iniParse { # Assign function parameters, defaults, and log debug info my ( $strOperation, $strContent, $bRelaxed, $bIgnoreInvalid, ) = logDebugParam ( __PACKAGE__ . '::iniParse', \@_, {name => 'strContent', required => false, trace => true}, {name => 'bRelaxed', optional => true, default => false, trace => true}, {name => 'bIgnoreInvalid', optional => true, default => false, trace => true}, ); # Ini content my $oContent = undef; my $strSection; # Create the JSON object my $oJSON = JSON::PP->new()->allow_nonref(); # Eval so exceptions can be ignored on bIgnoreInvalid eval { # Read the INI file foreach my $strLine (split("\n", defined($strContent) ? $strContent : '')) { $strLine = trim($strLine); # Skip lines that are blank or comments if ($strLine ne '' && $strLine !~ '^[ ]*#.*') { # Get the section if (index($strLine, '[') == 0) { $strSection = substr($strLine, 1, length($strLine) - 2); } else { if (!defined($strSection)) { confess &log(ERROR, "key/value pair '${strLine}' found outside of a section", ERROR_CONFIG); } # Get key and value my $iIndex = index($strLine, '='); if ($iIndex == -1) { confess &log(ERROR, "unable to find '=' in '${strLine}'", ERROR_CONFIG); } my $strKey = substr($strLine, 0, $iIndex); my $strValue = substr($strLine, $iIndex + 1); # If relaxed then read the value directly if ($bRelaxed) { if (defined($oContent->{$strSection}{$strKey})) { if (ref($oContent->{$strSection}{$strKey}) ne 'ARRAY') { $oContent->{$strSection}{$strKey} = [$oContent->{$strSection}{$strKey}]; } push(@{$oContent->{$strSection}{$strKey}}, $strValue); } else { $oContent->{$strSection}{$strKey} = $strValue; } } # Else read the value as stricter JSON else { ${$oContent}{$strSection}{$strKey} = $oJSON->decode($strValue); } } } } # Error if the file is empty if (!($bRelaxed || defined($oContent))) { confess &log(ERROR, 'no key/value pairs found', ERROR_CONFIG); } return true; } or do { # Confess the error if it should not be ignored if (!$bIgnoreInvalid) { confess $EVAL_ERROR; } # Undef content when errors are ignored undef($oContent); }; # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'oContent', value => $oContent, trace => true} ); } #################################################################################################################################### # save() - save the file. #################################################################################################################################### sub save { my $self = shift; # Save only if modified if ($self->{bModified}) { # Calculate the hash $self->hash(); # Save the file $self->{oStorage}->put($self->{strFileName}, iniRender($self->{oContent}), {strCipherPass => $self->{strCipherPass}}); $self->{oStorage}->pathSync(dirname($self->{strFileName})); $self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}), {strCipherPass => $self->{strCipherPass}}); $self->{oStorage}->pathSync(dirname($self->{strFileName})); $self->{bModified} = false; # Indicate the file now exists $self->{bExists} = true; # File was saved return true; } # File was not saved return false; } #################################################################################################################################### # saveCopy - save only a copy of the file. #################################################################################################################################### sub saveCopy { my $self = shift; if ($self->{oStorage}->exists($self->{strFileName})) { confess &log(ASSERT, "cannot save copy only when '$self->{strFileName}' exists"); } $self->hash(); $self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}), {strCipherPass => $self->{strCipherPass}}); } #################################################################################################################################### # iniRender() - render hash to standard INI format. #################################################################################################################################### push @EXPORT, qw(iniRender); sub iniRender { # Assign function parameters, defaults, and log debug info my ( $strOperation, $oContent, $bRelaxed, ) = logDebugParam ( __PACKAGE__ . '::iniRender', \@_, {name => 'oContent', trace => true}, {name => 'bRelaxed', default => false, trace => true}, ); # Open the ini file for writing my $strContent = ''; my $bFirst = true; # Create the JSON object canonical so that fields are alpha ordered to pass unit tests my $oJSON = JSON::PP->new()->canonical()->allow_nonref(); # Write the INI file foreach my $strSection (sort(keys(%$oContent))) { # Add a linefeed between sections if (!$bFirst) { $strContent .= "\n"; } # Write the section $strContent .= "[${strSection}]\n"; # Iterate through all keys in the section foreach my $strKey (sort(keys(%{$oContent->{$strSection}}))) { # If the value is a hash then convert it to JSON, otherwise store as is my $strValue = ${$oContent}{$strSection}{$strKey}; # If relaxed then store as old-style config if ($bRelaxed) { # If the value is an array then save each element to a separate key/value pair if (ref($strValue) eq 'ARRAY') { foreach my $strArrayValue (@{$strValue}) { $strContent .= "${strKey}=${strArrayValue}\n"; } } # Else write a standard key/value pair else { $strContent .= "${strKey}=${strValue}\n"; } } # Else write as stricter JSON else { $strContent .= "${strKey}=" . $oJSON->encode($strValue) . "\n"; } } $bFirst = false; } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'strContent', value => $strContent, trace => true} ); } #################################################################################################################################### # hash() - generate hash for the manifest. #################################################################################################################################### sub hash { my $self = shift; # Remove the old checksum delete($self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM}); # Set the new checksum $self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM} = cryptoHashOne('sha1', JSON::PP->new()->canonical()->allow_nonref()->encode($self->{oContent})); return $self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM}; } #################################################################################################################################### # get() - get a value. #################################################################################################################################### sub get { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $bRequired = shift; my $oDefault = shift; # Parameter constraints if (!defined($strSection)) { confess &log(ASSERT, 'strSection is required'); } if (defined($strSubKey) && !defined($strKey)) { confess &log(ASSERT, "strKey is required when strSubKey '${strSubKey}' is requested"); } # Get the result my $oResult = $self->{oContent}->{$strSection}; if (defined($strKey) && defined($oResult)) { $oResult = $oResult->{$strKey}; if (defined($strSubKey) && defined($oResult)) { $oResult = $oResult->{$strSubKey}; } } # When result is not defined if (!defined($oResult)) { # Error if a result is required if (!defined($bRequired) || $bRequired) { confess &log(ASSERT, "strSection '$strSection'" . (defined($strKey) ? ", strKey '$strKey'" : '') . (defined($strSubKey) ? ", strSubKey '$strSubKey'" : '') . ' is required but not defined'); } # Return default if specified if (defined($oDefault)) { return $oDefault; } } return $oResult } #################################################################################################################################### # boolGet() - get a boolean value. #################################################################################################################################### sub boolGet { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $bRequired = shift; my $bDefault = shift; return $self->get( $strSection, $strKey, $strSubKey, $bRequired, defined($bDefault) ? ($bDefault ? INI_TRUE : INI_FALSE) : undef) ? true : false; } #################################################################################################################################### # numericGet() - get a numeric value. #################################################################################################################################### sub numericGet { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $bRequired = shift; my $nDefault = shift; return $self->get($strSection, $strKey, $strSubKey, $bRequired, defined($nDefault) ? $nDefault + 0 : undef) + 0; } #################################################################################################################################### # set - set a value. #################################################################################################################################### sub set { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $oValue = shift; # Parameter constraints if (!(defined($strSection) && defined($strKey))) { confess &log(ASSERT, 'strSection and strKey are required'); } my $oCurrentValue; if (defined($strSubKey)) { $oCurrentValue = \$self->{oContent}{$strSection}{$strKey}{$strSubKey}; } else { $oCurrentValue = \$self->{oContent}{$strSection}{$strKey}; } if (!defined($$oCurrentValue) || defined($oCurrentValue) != defined($oValue) || ${dclone($oCurrentValue)} ne ${dclone(\$oValue)}) { $$oCurrentValue = $oValue; if (!$self->{bModified}) { $self->{bModified} = true; } return true; } return false; } #################################################################################################################################### # boolSet - set a boolean value. #################################################################################################################################### sub boolSet { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $bValue = shift; $self->set($strSection, $strKey, $strSubKey, $bValue ? INI_TRUE : INI_FALSE); } #################################################################################################################################### # numericSet - set a numeric value. #################################################################################################################################### sub numericSet { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; my $nValue = shift; $self->set($strSection, $strKey, $strSubKey, defined($nValue) ? $nValue + 0 : undef); } #################################################################################################################################### # remove - remove a value. #################################################################################################################################### sub remove { my $self = shift; my $strSection = shift; my $strKey = shift; my $strSubKey = shift; # Test if the value exists if ($self->test($strSection, $strKey, $strSubKey)) { # Remove a subkey if (defined($strSubKey)) { delete($self->{oContent}{$strSection}{$strKey}{$strSubKey}); } # Remove a key if (defined($strKey)) { if (!defined($strSubKey)) { delete($self->{oContent}{$strSection}{$strKey}); } # Remove the section if it is now empty if (keys(%{$self->{oContent}{$strSection}}) == 0) { delete($self->{oContent}{$strSection}); } } # Remove a section if (!defined($strKey)) { delete($self->{oContent}{$strSection}); } # Record changes if (!$self->{bModified}) { $self->{bModified} = true; } return true; } return false; } #################################################################################################################################### # keys - get the list of keys in a section. #################################################################################################################################### sub keys { my $self = shift; my $strSection = shift; my $strSortOrder = shift; if ($self->test($strSection)) { if (!defined($strSortOrder) || $strSortOrder eq INI_SORT_FORWARD) { return (sort(keys(%{$self->get($strSection)}))); } elsif ($strSortOrder eq INI_SORT_REVERSE) { return (sort {$b cmp $a} (keys(%{$self->get($strSection)}))); } elsif ($strSortOrder eq INI_SORT_NONE) { return (keys(%{$self->get($strSection)})); } else { confess &log(ASSERT, "invalid strSortOrder '${strSortOrder}'"); } } my @stryEmptyArray; return @stryEmptyArray; } #################################################################################################################################### # test - test a value. # # Test a value to see if it equals the supplied test value. If no test value is given, tests that the section, key, or subkey # is defined. #################################################################################################################################### sub test { my $self = shift; my $strSection = shift; my $strValue = shift; my $strSubValue = shift; my $strTest = shift; # Get the value my $strResult = $self->get($strSection, $strValue, $strSubValue, false); # Is there a result if (defined($strResult)) { # Is there a value to test against? if (defined($strTest)) { # Make sure these are explicit strings or Devel::Cover thinks they are equal if one side is a boolean return ($strResult . '') eq ($strTest . '') ? true : false; } return true; } return false; } #################################################################################################################################### # boolTest - test a boolean value, see test(). #################################################################################################################################### sub boolTest { my $self = shift; my $strSection = shift; my $strValue = shift; my $strSubValue = shift; my $bTest = shift; return $self->test($strSection, $strValue, $strSubValue, defined($bTest) ? ($bTest ? INI_TRUE : INI_FALSE) : undef); } #################################################################################################################################### # cipherPassSub - gets the passphrase (if it exists) used to read/write subsequent files #################################################################################################################################### sub cipherPassSub { my $self = shift; return $self->get(INI_SECTION_CIPHER, INI_KEY_CIPHER_PASS, undef, false); } #################################################################################################################################### # Properties. #################################################################################################################################### sub modified {shift->{bModified}} # Has the data been modified since last load/save? sub exists {shift->{bExists}} # Is the data persisted to file? sub cipherPass {shift->{strCipherPass}} # Return passphrase (will be undef if repo not encrypted) 1;