#!/usr/bin/env perl # Copyright (c) 2026, 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. # # checkConfig - Validate sneakernet configuration structure against template # # PURPOSE: # Compares the structure of a sneakernet YAML configuration file against the # datastructure template to identify missing keys, extra keys, and type mismatches. # Useful for configuration validation and detecting configuration drift. # # USAGE: # Standalone: ./checkConfig # From Perl script: my ($results, $errors) = do './checkConfig'; # # BEHAVIOR: # 1. Loads the sneakernet.datastructure template (Perl hash structure) # 2. Loads the sneakernet.conf.yaml configuration file (YAML) # 3. Recursively extracts all keys and their types from both structures # 4. Compares structures and identifies: # - Keys only in config (extra configuration) # - Keys only in datastructure (missing from config) # - Keys with different types (type mismatches) # - Common keys (matching structure) # 5. Reports summary with counts and detailed listings # # OUTPUT: # - Summary counts of each difference type # - List of keys only in config file (e.g., additional datasets) # - List of keys only in template (missing configuration) # - List of keys with type mismatches # - Total key counts and match percentage # # FILES: # Input: ./sneakernet.datastructure (Perl data structure) # Input: ./sneakernet.conf.yaml (YAML configuration) # NOTE: Adjust paths as needed based on script location. # # RETURN VALUE: # When called from another script: ($results, $errors) # - $results: String containing comparison report # - $errors: String containing error messages (empty if none) # # REQUIREMENTS: # - Perl with YAML::Tiny module installed # - Access to both datastructure and config files # use strict; use warnings; use YAML::Tiny; use Data::Dumper; # YAML::Tiny wrapper function my $loadFile = sub { my $yaml = YAML::Tiny->read($_[0]); return $yaml->[0]; }; my $datasetFile = './sneakernet.datastructure'; my $configFile = './sneakernet.conf.yaml'; sub getStructure { my ($data, $prefix) = @_; $prefix //= ''; my %structure; if (ref($data) eq 'HASH') { foreach my $key (sort keys %$data) { my $path = $prefix ? "$prefix.$key" : $key; $structure{$path} = ref($data->{$key}) || 'SCALAR'; if (ref($data->{$key})) { my %sub = getStructure($data->{$key}, $path); %structure = (%structure, %sub); } } } elsif (ref($data) eq 'ARRAY') { if (@$data) { my $path = $prefix . '[0]'; $structure{$path} = ref($data->[0]) || 'SCALAR'; if (ref($data->[0])) { my %sub = getStructure($data->[0], $path); %structure = (%structure, %sub); } } } return %structure; } # Load YAML config die "Config file not found: $configFile\n" unless -f $configFile; my $configData = $loadFile->($configFile); my %configStructure = getStructure($configData); # Load dataset (Perl data structure file) die "Dataset file not found: $datasetFile\n" unless -f $datasetFile; my $datasetData = do $datasetFile; die "Error loading dataset file: $@\n" if $@; die "Dataset file did not return a value\n" unless defined $datasetData; my %datasetStructure = getStructure($datasetData); # Compare structures my %allKeys = map { $_ => 1 } (keys %configStructure, keys %datasetStructure); my @keysOnlyInConfig = sort grep { !exists $datasetStructure{$_} } keys %configStructure; my @keysOnlyInDataset = sort grep { !exists $configStructure{$_} } keys %datasetStructure; my @keysWithDifferentTypes = sort grep { exists $configStructure{$_} && exists $datasetStructure{$_} && $configStructure{$_} ne $datasetStructure{$_} } keys %allKeys; my @commonKeys = sort grep { exists $configStructure{$_} && exists $datasetStructure{$_} && $configStructure{$_} eq $datasetStructure{$_} } keys %allKeys; # Build output in an array my @results; my @errors; push @results, "Structure Comparison Summary"; push @results, "=" x 60; push @results, ""; push @results, sprintf("Keys only in %s: %d", $configFile, scalar @keysOnlyInConfig); push @results, sprintf("Keys only in %s: %d", $datasetFile, scalar @keysOnlyInDataset); push @results, sprintf("Keys with different types: %d", scalar @keysWithDifferentTypes); push @results, sprintf("Common keys: %d", scalar @commonKeys); if (@keysOnlyInConfig) { push @results, ""; push @results, "Keys only in $configFile:"; foreach my $key (@keysOnlyInConfig) { push @results, " $key"; } } if (@keysOnlyInDataset) { push @results, ""; push @results, "Keys only in $datasetFile:"; foreach my $key (@keysOnlyInDataset) { push @results, " $key"; } } if (@keysWithDifferentTypes) { push @results, ""; push @results, "Keys with different types:"; foreach my $key (@keysWithDifferentTypes) { push @results, " $key: $configFile=$configStructure{$key}, $datasetFile=$datasetStructure{$key}"; } } my $totalConfigKeys = keys %configStructure; my $totalDatasetKeys = keys %datasetStructure; push @results, ""; push @results, sprintf("Total keys in %s: %d", $configFile, $totalConfigKeys); push @results, sprintf("Total keys in %s: %d", $datasetFile, $totalDatasetKeys); push @results, sprintf("Match percentage: %.1f%%", (scalar @commonKeys / $totalDatasetKeys * 100)); # Return or print results depending on context if (caller()) { # called from another script return ( join( "\n", @results ) . "\n", @errors ? join("\n", @errors) : "" ); } else { # run standalone print "Results Summary:\n"; print join( "\n", @results ) . "\n"; if (@errors) { print "\n=== CLEANUP SCRIPT ERRORS ===\n"; print join("\n", @errors) . "\n"; } } # End of script