Skip to content

Commit

Permalink
fix: Refacto XML parsing of plist files on MacOSX
Browse files Browse the repository at this point in the history
  • Loading branch information
g-bougard committed May 3, 2024
1 parent f58d560 commit 4e52de0
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 25 deletions.
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ core:
* Refacto events management to permit a task to trigger another one
* IPC long messages only have size limitation on windows
* Add new IPC message type to handle too long IPC_EVENT messages on windows
* Refacto XML parsing of plist files on MacOSX

inventory:
* Fix network default route discovery on linux
Expand Down
37 changes: 27 additions & 10 deletions lib/GLPI/Agent/Tools/MacOS.pm
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ sub _getSystemProfilerInfosXML {
);
} elsif ($params{type} =~ /^SP(SerialATA|DiscBurning|CardReader|USB|FireWire)DataType$/) {
$info->{storages} = _extractStoragesFromXml(
type => $params{type},
string => $xmlStr,
logger => $params{logger}
);
Expand All @@ -49,31 +50,32 @@ sub _getSystemProfilerInfosXML {
sub _getDict {
my (%params) = @_;

my $dict = $params{dict} // '_items';

my $xml = GLPI::Agent::XML->new(
is_plist => 1,
%params
)->dump_as_hash();

return unless $xml && ref($xml->{plist}->{array}[0]->{dict}[0]->{array}) eq 'ARRAY';

my $node = first { ref($_) eq 'HASH' && exists($_->{dict}) } @{$xml->{plist}->{array}[0]->{dict}[0]->{array}};
return unless $xml && ref($xml->{plist}) eq 'ARRAY' && ref($xml->{plist}->[0]) eq 'HASH' && ref($xml->{plist}->[0]->{$dict}) eq 'ARRAY';

return $node->{dict};
return $xml->{plist}->[0]->{$dict};
}

sub _recSubStorage {
my ($list) = @_;
my ($list, $sublistkey, $depth) = @_;

my $listkey = $depth && !empty($sublistkey) ? $sublistkey : "_items";

my @nodes;
foreach my $node (@{$list}) {
next unless ref($node) eq 'HASH';
if ($node->{array} && ref($node->{array}[0]) eq 'HASH' && exists($node->{array}[0]->{dict})) {
push @nodes, map { _recSubStorage($_->{dict}) }
grep { ref($_) eq 'HASH' && exists($_->{dict}) } @{$node->{array}};
if ($listkey && ref($node->{$listkey}) eq 'ARRAY') {
push @nodes, _recSubStorage($node->{$listkey}, $sublistkey, $depth+1);
}
if ($node->{_name}) {
# Always cleanup from subnodes
delete $node->{array};
delete $node->{$listkey};
push @nodes, $node;
}
}
Expand All @@ -84,12 +86,15 @@ sub _recSubStorage {
sub _extractStoragesFromXml {
my (%params) = @_;

my $type = delete $params{type} // '';
my $sublistkey = $type eq 'SPFireWireDataType' ? 'units' : '_items';

my $dict = _getDict(%params)
or return;

my $storages = {};

foreach my $storage (_recSubStorage($dict)) {
foreach my $storage (_recSubStorage($dict, $sublistkey, 0)) {
my $name = $storage->{_name}
or next;
$storages->{$name} = $storage;
Expand Down Expand Up @@ -129,6 +134,8 @@ sub _extractSoftwaresFromXml {

my $softwares = {};

return unless ref($softlist) eq 'ARRAY';

foreach my $soft (@{$softlist}) {

my $name = $soft->{_name}
Expand Down Expand Up @@ -167,6 +174,16 @@ sub _extractSoftwaresFromXml {
$entry->{'Last Modified'} = _getOffsetDate($lastmod, $params{localTimeOffset})
if defined($lastmod);

# Keep only meaningful signed_by element
if (ref($soft->{'signed_by'}) eq 'ARRAY') {
$entry->{'Signed by'} = first { /^Developer ID Application:/ } @{$soft->{'signed_by'}};
}

# Mimic plaintext output format extraction
if ($soft->{'obtained_from'} && $soft->{'obtained_from'} eq 'identified_developer') {
$entry->{'Obtained from'} = "Identified Developer";
}

my %mapping = (
version => 'Version',
path => 'Location',
Expand Down
42 changes: 27 additions & 15 deletions lib/GLPI/Agent/XML.pm
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ sub new {
threaded
);

# Support required by GLPI::Agent::Tools::MacOS
$self->{_force_array} = [ qw(array dict) ] if $self->{_is_plist};

return $self;
}

Expand Down Expand Up @@ -302,15 +299,30 @@ sub dump_as_hash {

my $ret;
if ($type == XML::LibXML::XML_ELEMENT_NODE()) { # 1
my $current_plist_key;
my $textkey = $self->{_text_node_key} // '#text';
my $force_array = $self->{_force_array};
my $skip_attr = $self->{_skip_attr};
my $plist = $self->{_is_plist};
my $name = $node->nodeName;
foreach my $leaf (map { $self->dump_as_hash($_) } $node->childNodes()) {
if (ref($leaf) eq 'HASH') {
foreach my $child ($node->childNodes()) {
my $leaf = $self->dump_as_hash($child);
if ($plist) {
if ($name eq "array") {
$ret = [] unless ref($ret) eq 'ARRAY';
push @{$ret}, $leaf;
} elsif ($name eq "dict") {
if (defined($current_plist_key)) {
$ret->{$current_plist_key} = $leaf;
undef $current_plist_key;
} else {
$current_plist_key = $leaf;
}
} else {
$ret = $leaf;
}
} elsif (ref($leaf) eq 'HASH') {
foreach my $key (keys(%{$leaf})) {
next if $plist && $key =~ /^key|string|date|integer|real|data|true|false$/;
# Transform key in array ref is necessary
if (exists($ret->{$name}->{$key})) {
$ret->{$name}->{$key} = [ $ret->{$name}->{$key} ]
Expand All @@ -321,27 +333,27 @@ sub dump_as_hash {
$ret->{$name}->{$key} = $as_array ? [ $leaf->{$key} ] : $leaf->{$key};
}
}
} elsif ($plist) {
if ($name eq "key") {
$self->{_current_name} = $leaf;
} elsif ($self->{_current_name}) {
$ret->{$self->{_current_name}} = $leaf;
delete $self->{_current_name};
}
} elsif (!ref($ret->{$name})) {
$ret->{$name}->{$textkey} .= $leaf;
} elsif ($leaf) {
warn "GLPI::Agent::XML: Unsupported value type for $name: '$leaf'".(ref($leaf) ? " (".ref($leaf).")" : "")."\n";
}
}
unless ($skip_attr) {
# We should skip XML attributs when reading a MacOSX plist file
unless ($plist || $skip_attr) {
my $attr_prefix = $self->{_attr_prefix} // "-";
foreach my $attribute ($node->attributes()) {
my $attr = $attr_prefix.$attribute->nodeName();
$ret->{$name}->{$attr} = $attribute->getValue();
}
}
if (!defined($ret)) {
if ($plist) {
if ($name eq 'array') {
$ret = [] unless defined($ret);
} elsif ($name !~ /^key|string|date|integer|real|data|true|false|array|dict$/) {
$ret = { $name => $ret };
}
} elsif (!defined($ret)) {
$ret->{$name} = '';
} elsif (defined($ret->{$name}->{$textkey}) && keys(%{$ret->{$name}}) == 1) {
my $as_array = ref($force_array) eq 'ARRAY' && any { $name eq $_ } @{$force_array};
Expand Down
132 changes: 132 additions & 0 deletions t/agent/xml.t
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use File::Temp qw(tempdir);
use UNIVERSAL::require;
use Encode qw(decode);
use XML::LibXML;
use Data::Dumper;

use GLPI::Agent::XML;
use GLPI::Agent::Tools;
Expand Down Expand Up @@ -347,6 +348,7 @@ File::Find::find({
# Skip files wrongly handled by XML::TreePP in specific case we don't care about
my @skip_treepp_test = qw(
resources/esx/esx-4.1.0-1/RetrieveProperties.soap
resources/macos/system_profiler/SPCardReaderDataType2.xml
);

my %file_cases = (
Expand Down Expand Up @@ -385,6 +387,131 @@ my %file_cases = (
}
},
},
"resources/macos/system_profiler/SPCardReaderDataType2.xml" => {
options => {
is_plist => 1,
},
dump => {
plist => [
{
_SPCommandLineArguments => [
'/usr/sbin/system_profiler',
'-nospawn',
'-xml',
'SPCardReaderDataType',
'-detailLevel',
'full'
],
_SPCompletionInterval => '0.060505032539367676',
_SPResponseTime => '0.079851984977722168',
_dataType => 'SPCardReaderDataType',
_detailLevel => -1,
_items => [],
_parentDataType => 'SPHardwareDataType',
_properties => {
_name => {
_isColumn => 'NO',
_isOutlineColumn => 'NO',
_order => 0
},
bsd_name => {
_order => 260
},
detachable_drive => {
_order => 240
},
file_system => {
_order => 250
},
free_space => {
_deprecated => undef,
_order => 200
},
free_space_in_bytes => {
_isByteSize => undef,
_order => 200
},
mount_point => {
_order => 270
},
removable_media => {
_order => 220
},
size => {
_deprecated => undef,
_order => 210
},
size_in_bytes => {
_isByteSize => undef,
_order => 210
},
spcardreader => {
_order => 10
},
'spcardreader_card_manufacturer-id' => {
_order => 140
},
spcardreader_card_manufacturing_date => {
_order => 170
},
spcardreader_card_oemid => {
_order => 130
},
spcardreader_card_productname => {
_order => 120
},
spcardreader_card_productrevision => {
_order => 150
},
spcardreader_card_serialnumber => {
_order => 160
},
spcardreader_card_specversion => {
_order => 180
},
'spcardreader_device-id' => {
_order => 50
},
'spcardreader_link-speed' => {
_order => 110
},
'spcardreader_link-width' => {
_order => 100
},
'spcardreader_product-id' => {
_order => 40
},
'spcardreader_revision-id' => {
_order => 80,
_suppressLocalization => undef
},
spcardreader_serialnumber => {
_order => 90
},
'spcardreader_subsystem-id' => {
_order => 70
},
'spcardreader_subsystem_vendor-id' => {
_order => 60
},
'spcardreader_vendor-id' => {
_order => 30
},
volumes => {
_detailLevel => 0
},
writable => {
_order => 230
}
},
_timeStamp => '2022-10-06T16:43:05Z',
_versionInfo => {
'com.apple.SystemProfiler.SPCardReaderReporter' => '2.7'
}
}
]
}
},
);

my $textkey = '#text';
Expand Down Expand Up @@ -495,6 +622,11 @@ foreach my $file (sort @xml_files) {
$file_cases{$file}->{dump},
"<$file> file expected hash"
);
unless (keys(%{$file_cases{$file}}) && $file_cases{$file}->{dump}) {
my $dumper = Data::Dumper->new([{dump => $dump}], ["hash"])->Useperl(1)->Indent(1)->Quotekeys(0)->Sortkeys(1)->Pad(" ");
$dumper->{xpad} = " ";
print STDERR "====\nCURRENT DUMP: ", $dumper->Dump();
}
}

if ($tpp && !grep { $_ eq $file } @skip_treepp_test) {
Expand Down

0 comments on commit 4e52de0

Please sign in to comment.