mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2025-01-30 05:39:12 +02:00
c002a2ce2f
Putting the checksum at the beginning of the file made it impossible to stream the file out when saving. The entire file had to be held in memory while it was checksummed so the checksum could be written at the beginning. Instead place the checksum at the end. This does not break the existing Perl or C code since the read is not order dependent. There are no plans to improve the Perl code to take advantage of this change, but it will make the C implementation more efficient. Reviewed by Cynthia Shang.
882 lines
31 KiB
Perl
882 lines
31 KiB
Perl
####################################################################################################################################
|
|
# 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
|
|
{
|
|
# Skip the checksum for now but write all other key/value pairs
|
|
if (!($strSection eq INI_SECTION_BACKREST && $strKey eq INI_KEY_CHECKSUM))
|
|
{
|
|
$strContent .= "${strKey}=" . $oJSON->encode($strValue) . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
$bFirst = false;
|
|
}
|
|
|
|
# If there is a checksum write it at the end of the file. Having the checksum at the end of the file allows some major
|
|
# performance optimizations which we won't implement in Perl, but will make the C code much more efficient.
|
|
if (!$bRelaxed && defined($oContent->{&INI_SECTION_BACKREST}) && defined($oContent->{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM}))
|
|
{
|
|
$strContent .=
|
|
"\n[" . INI_SECTION_BACKREST . "]\n" .
|
|
INI_KEY_CHECKSUM . '=' . $oJSON->encode($oContent->{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM}) . "\n";
|
|
}
|
|
|
|
# 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;
|