mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2024-12-12 10:04:14 +02:00
de7fc37f88
Refactor storage layer to allow for new repository filesystems using drivers. (Reviewed by Cynthia Shang.) Refactor IO layer to allow for new compression formats, checksum types, and other capabilities using filters. (Reviewed by Cynthia Shang.)
825 lines
27 KiB
Perl
825 lines
27 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 Digest::SHA;
|
|
use Exporter qw(import);
|
|
our @EXPORT = qw();
|
|
use Fcntl qw(:mode O_WRONLY O_CREAT O_TRUNC);
|
|
use File::Basename qw(dirname basename);
|
|
use IO::Handle;
|
|
use JSON::PP;
|
|
use Storable qw(dclone);
|
|
|
|
use pgBackRest::Common::Exception;
|
|
use pgBackRest::Common::Log;
|
|
use pgBackRest::Common::String;
|
|
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);
|
|
|
|
####################################################################################################################################
|
|
# 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,
|
|
) =
|
|
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 => BACKREST_FORMAT, trace => true},
|
|
{name => 'strInitVersion', optional => true, default => BACKREST_VERSION, trace => true},
|
|
{name => 'bIgnoreMissing', optional => true, default => false, trace => true},
|
|
);
|
|
|
|
# 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 from string and 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});
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# loadVersion() - load a version (main or copy) of the ini file
|
|
####################################################################################################################################
|
|
sub loadVersion
|
|
{
|
|
my $self = shift;
|
|
my $bCopy = shift;
|
|
my $bIgnoreError = shift;
|
|
|
|
# Load main
|
|
my $rstrContent = $self->{oStorage}->get(
|
|
$self->{oStorage}->openRead($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''), {bIgnoreMissing => $bIgnoreError}));
|
|
|
|
# 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});
|
|
}
|
|
}
|
|
}
|
|
|
|
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}));
|
|
$self->{oStorage}->pathSync(dirname($self->{strFileName}));
|
|
$self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}));
|
|
$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}));
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# 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});
|
|
|
|
# Calculate the checksum
|
|
my $oSHA = Digest::SHA->new('sha1');
|
|
my $oJSON = JSON::PP->new()->canonical()->allow_nonref();
|
|
$oSHA->add($oJSON->encode($self->{oContent}));
|
|
|
|
# Set the new checksum
|
|
$self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM} = $oSHA->hexdigest();
|
|
|
|
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))
|
|
{
|
|
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);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# Properties.
|
|
####################################################################################################################################
|
|
sub modified {shift->{bModified}} # Has the data been modified since last load/save?
|
|
sub exists {shift->{bExists}} # Is the data persisted to file?
|
|
|
|
1;
|