#!/usr/bin/env perl # Copyright (c) 2025, R. W. Rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # compare_reports # Compares old and new sysinfo reports for v3.7.1 compatibility rules. # # Revision History: # 2026-10-11 Copilot v1.0.0 r? initial v3.8.0 compatibility comparator # 2026-10-12 Copilot v1.0.1 r? segment-safe path handling for dotted keys use warnings; use strict; use version; our $VERSION = version->declare('v1.0.1'); use YAML::Tiny; ####################################################### # # usage() # # Prints usage and exits with error. # # Parameters: # none # # Returns: # none # # Side Effects: # terminates process with die # ####################################################### sub usage { die "Usage: compare_reports OLD.yaml NEW.yaml\n"; } ####################################################### # # loadReport( $path ) # # Loads YAML report file while ignoring optional sysinfo # comment preamble line. # # Parameters: # $path - report file path # # Returns: # hash reference report document # # Side Effects: # reads from filesystem # ####################################################### sub loadReport { my $path = shift; open my $fh, '<', $path or die "Could not read $path: $!\n"; my @lines = <$fh>; close($fh); if (@lines && $lines[0] =~ /^#sysinfo:\s+/) { shift @lines; } my $yaml = YAML::Tiny->read_string(join('', @lines)); die "Could not parse YAML from $path\n" unless $yaml && ref($yaml->[0]) eq 'HASH'; return $yaml->[0]; } ####################################################### # # nodeType( $value ) # # Returns normalized node type string for scalar, map, # or list. # # Parameters: # $value - perl data value # # Returns: # scalar node type string # # Side Effects: # none # ####################################################### sub nodeType { my $value = shift; return 'map' if ref($value) eq 'HASH'; return 'list' if ref($value) eq 'ARRAY'; return 'scalar'; } ####################################################### # # pathKey( $segments ) # # Builds an internal unique path key from path segments. # # Parameters: # $segments - array reference of path segments # # Returns: # scalar key string joined by tab # # Side Effects: # none # ####################################################### sub pathKey { my $segments = shift; return join("\t", @{$segments}); } ####################################################### # # pathDisplay( $segments ) # # Converts path segments to display string. # # Parameters: # $segments - array reference of path segments # # Returns: # scalar display path joined by dot # # Side Effects: # none # ####################################################### sub pathDisplay { my $segments = shift; return join('.', @{$segments}); } ####################################################### # # flattenTypesAndValues( $node, $segments, $types, $values, $paths ) # # Flattens nested report data into type/value maps keyed by # internal segment-safe path keys. # # Parameters: # $node - current node # $segments - array reference of current path segments # $types - hash reference pathKey => type # $values - hash reference pathKey => scalar value # $paths - hash reference pathKey => segment array # # Returns: # none # # Side Effects: # populates hashes by reference # ####################################################### sub flattenTypesAndValues { my ($node, $segments, $types, $values, $paths) = @_; my $type = nodeType($node); if (@{$segments}) { my $key = pathKey($segments); $types->{$key} = $type; $paths->{$key} = [@{$segments}]; if ($type eq 'scalar') { $values->{$key} = defined($node) ? "$node" : ''; } } if ($type eq 'map') { foreach my $key (sort keys %{$node}) { my @childPath = (@{$segments}, $key); flattenTypesAndValues($node->{$key}, \@childPath, $types, $values, $paths); } } elsif ($type eq 'list') { for (my $i = 0; $i < @{$node}; $i++) { my @childPath = (@{$segments}, $i); flattenTypesAndValues($node->[$i], \@childPath, $types, $values, $paths); } } } ####################################################### # # isAllowedDifference( $segments ) # # Determines whether a scalar path is in the allowed # difference list from Spec section 1.2. # # Parameters: # $segments - array reference of path segments # # Returns: # boolean true when difference is allowed # # Side Effects: # none # ####################################################### sub isAllowedDifference { my $segments = shift; return 1 if @{$segments} == 2 && $segments->[0] eq 'report' && ($segments->[1] eq 'date' || $segments->[1] eq 'version'); return 1 if @{$segments} == 3 && $segments->[0] eq 'software' && $segments->[1] eq 'sysinfo-client' && $segments->[2] eq 'version'; return 1 if @{$segments} == 2 && $segments->[0] eq 'system' && ($segments->[1] eq 'uptime' || $segments->[1] eq 'last_boot' || $segments->[1] eq 'Power Draw'); return 1 if @{$segments} == 2 && $segments->[0] eq 'system' && $segments->[1] =~ /^Power Current \d+$/; return 1 if @{$segments} == 2 && $segments->[0] eq 'system' && $segments->[1] =~ /^Power Voltage \d+$/; return 1 if @{$segments} == 3 && $segments->[0] eq 'diskinfo' && ($segments->[2] eq 'used' || $segments->[2] eq 'size'); return 1 if @{$segments} == 2 && $segments->[0] eq 'attributes' && $segments->[1] =~ / (uptime|writes|Sector Size)$/; return 1 if @{$segments} == 4 && $segments->[0] eq 'xen' && $segments->[1] eq 'virtual' && ($segments->[3] eq 'memory' || $segments->[3] eq 'numcpu'); return 0; } usage() unless @ARGV == 2; my ($oldPath, $newPath) = @ARGV; my $old = loadReport($oldPath); my $new = loadReport($newPath); my (%oldTypes, %newTypes, %oldValues, %newValues, %oldPaths, %newPaths); flattenTypesAndValues($old, [], \%oldTypes, \%oldValues, \%oldPaths); flattenTypesAndValues($new, [], \%newTypes, \%newValues, \%newPaths); my @missing; my @typeChanges; my @valueDiffs; foreach my $pathKey (sort keys %oldTypes) { if (!exists $newTypes{$pathKey}) { push @missing, pathDisplay($oldPaths{$pathKey}); next; } if ($newTypes{$pathKey} ne $oldTypes{$pathKey}) { my $display = pathDisplay($oldPaths{$pathKey}); push @typeChanges, "$display old=$oldTypes{$pathKey} new=$newTypes{$pathKey}"; next; } next unless $oldTypes{$pathKey} eq 'scalar'; my $oldValue = exists $oldValues{$pathKey} ? $oldValues{$pathKey} : ''; my $newValue = exists $newValues{$pathKey} ? $newValues{$pathKey} : ''; next if $oldValue eq $newValue; next if isAllowedDifference($oldPaths{$pathKey}); push @valueDiffs, pathDisplay($oldPaths{$pathKey}); } print "Missing old key paths in NEW:\n"; print scalar(@missing) ? join("\n", map { " $_" } @missing) . "\n" : " (none)\n"; print "Type changes:\n"; print scalar(@typeChanges) ? join("\n", map { " $_" } @typeChanges) . "\n" : " (none)\n"; print "Value differences outside allowed list:\n"; print scalar(@valueDiffs) ? join("\n", map { " $_" } @valueDiffs) . "\n" : " (none)\n"; exit((@missing || @typeChanges) ? 1 : 0);