Skip to content

Instantly share code, notes, and snippets.

@manwar
Created April 11, 2025 22:21
Show Gist options
  • Save manwar/c6ff473380f5e85622a128dbec60d015 to your computer and use it in GitHub Desktop.
Save manwar/c6ff473380f5e85622a128dbec60d015 to your computer and use it in GitHub Desktop.
Verify CHECKSUMS of given module and optionally version.
#!/usr/bin/env perl
use v5.30;
use Safe;
use JSON;
use LWP::UserAgent;
use Digest::SHA qw(sha256_hex);
our $VERBOSE = 0;
my ($module, $version) = @ARGV;
die "Usage: $0 Module::Name [version]" unless $module;
my $download_url = get_download_url($module, $version);
my ($tarball) = $download_url =~ m|/([^/]+\.tar\.gz)$|;
die "Failed to extract tarball name from URL" unless $tarball;
my $local_sha256 = get_local_sha256($download_url);
print "SHA256 from downloaded tarball: $local_sha256\n" if $VERBOSE;
my $cpan_sha256 = get_cpan_sha256($module, $tarball);
print "SHA256 from CPAN CHECKSUMS : $cpan_sha256\n" if $VERBOSE;
if ($cpan_sha256 eq $local_sha256) {
print "Checksums: PASS\n";
} else {
print "Checksums: FAIL\n";
}
#
#
# SUBROUTINES
sub get_local_sha256 {
my ($url) = @_;
my $ua = LWP::UserAgent->new;
my $res = $ua->get($url);
die "Download failed: " . $res->status_line
unless $res->is_success;
return sha256_hex($res->decoded_content);
}
sub get_cpan_sha256 {
my ($module, $tarball) = @_;
my $mod_info = get_module_info($module);
my $author = $mod_info->{author};
my $checksums = fetch_checksums_data($author);
if (exists $checksums->{$tarball}{'sha256'}) {
return $checksums->{$tarball}{'sha256'};
} else {
die "No SHA256 found for $tarball in CHECKSUMS\n";
}
}
sub get_module_info {
my ($module) = @_;
my $ua = LWP::UserAgent->new;
my $url = "https://fastapi.metacpan.org/v1/module/$module";
my $res = $ua->get($url);
die "Module fetch error: " . $res->status_line
unless $res->is_success;
return decode_json($res->decoded_content);
}
sub get_download_url {
my ($module, $version) = @_;
# Step 1: Get distribution name
my $ua = LWP::UserAgent->new;
my $mod_url = "https://fastapi.metacpan.org/v1/module/$module";
my $mod_res = $ua->get($mod_url);
die "Error fetching module info: " . $mod_res->status_line
unless $mod_res->is_success;
my $mod_data = decode_json($mod_res->decoded_content);
my $dist = $mod_data->{distribution};
# Step 2: Search for release
my $query_url = "https://fastapi.metacpan.org/v1/release/_search";
my $query = {
query => {
bool => {
must => [
{ term => { distribution => $dist } },
($version ? ({ term => { version => $version } }) : ()),
]
}
},
size => 1,
sort => [ { date => "desc" } ],
};
my $res = $ua->post(
$query_url,
'Content-Type' => 'application/json',
Content => encode_json($query)
);
die "Error searching release: " . $res->status_line
unless $res->is_success;
my $data = decode_json($res->decoded_content);
if (ref $data->{hits} eq 'HASH' &&
ref $data->{hits}{hits} eq 'ARRAY' &&
@{$data->{hits}{hits}}) {
my $release = $data->{hits}{hits}[0]{_source};
return $release->{download_url};
} else {
die "No matching release found for $dist" .
($version ? " version $version" : "");
}
}
sub fetch_checksums_data {
my ($author) = @_;
my @chars = split //, $author;
my $url = sprintf(
"https://cpan.metacpan.org/authors/id/%s/%s/%s/CHECKSUMS",
$chars[0], $chars[0] . $chars[1], $author
);
my $ua = LWP::UserAgent->new;
my $res = $ua->get($url);
die "Failed to fetch CHECKSUMS: " . $res->status_line
unless $res->is_success;
my $safe = Safe->new;
my $data = $safe->reval($res->decoded_content);
die "Failed to eval CHECKSUMS"
unless ref $data eq 'HASH';
return $data;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment