From 4e52de0715dce1a9aa5177ab88f34c6fd20a29c8 Mon Sep 17 00:00:00 2001 From: Guillaume Bougard Date: Fri, 3 May 2024 17:44:56 +0200 Subject: [PATCH] fix: Refacto XML parsing of plist files on MacOSX --- Changes | 1 + lib/GLPI/Agent/Tools/MacOS.pm | 37 +++++++--- lib/GLPI/Agent/XML.pm | 42 +++++++---- t/agent/xml.t | 132 ++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 25 deletions(-) diff --git a/Changes b/Changes index 92a43b0da..8ad261c68 100644 --- a/Changes +++ b/Changes @@ -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 diff --git a/lib/GLPI/Agent/Tools/MacOS.pm b/lib/GLPI/Agent/Tools/MacOS.pm index 3e5d64a0d..6ae89f702 100644 --- a/lib/GLPI/Agent/Tools/MacOS.pm +++ b/lib/GLPI/Agent/Tools/MacOS.pm @@ -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} ); @@ -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; } } @@ -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; @@ -129,6 +134,8 @@ sub _extractSoftwaresFromXml { my $softwares = {}; + return unless ref($softlist) eq 'ARRAY'; + foreach my $soft (@{$softlist}) { my $name = $soft->{_name} @@ -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', diff --git a/lib/GLPI/Agent/XML.pm b/lib/GLPI/Agent/XML.pm index de43a3fb8..b48214bbd 100644 --- a/lib/GLPI/Agent/XML.pm +++ b/lib/GLPI/Agent/XML.pm @@ -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; } @@ -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} ] @@ -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}; diff --git a/t/agent/xml.t b/t/agent/xml.t index e8bc6ca9f..480d5bb54 100644 --- a/t/agent/xml.t +++ b/t/agent/xml.t @@ -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; @@ -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 = ( @@ -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'; @@ -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) {