provisioning/tools/package/generate-checksums.nu
2025-10-07 11:12:02 +01:00

530 lines
17 KiB
Plaintext

#!/usr/bin/env nu
# Checksum generation tool - generates and verifies checksums for distribution packages
#
# Generates:
# - SHA256 checksums (primary)
# - MD5 checksums (legacy compatibility)
# - SHA512 checksums (high security)
# - File integrity verification
use std log
def main [
--input-dir: string = "packages" # Directory containing packages to checksum
--output-file: string = "" # Output file for checksums (auto-generated if empty)
--algorithms: string = "sha256,md5" # Hash algorithms: sha256, md5, sha512, all
--format: string = "standard" # Output format: standard, json, csv
--verify: string = "" # Verify checksums from existing file
--recursive: bool = false # Process directories recursively
--pattern: string = "*" # File pattern to match
--verbose: bool = false # Enable verbose logging
] -> record {
let input_root = ($input_dir | path expand)
let algorithms_list = if $algorithms == "all" {
["sha256", "md5", "sha512"]
} else {
($algorithms | split row "," | each { str trim })
}
let checksum_config = {
input_dir: $input_root
output_file: (if $output_file == "" {
($input_root | path join $"checksums-($algorithms_list.0).txt")
} else {
$output_file | path expand
})
algorithms: $algorithms_list
format: $format
verify_file: (if $verify == "" { "" } else { $verify | path expand })
recursive: $recursive
pattern: $pattern
verbose: $verbose
}
log info $"Starting checksum generation with config: ($checksum_config)"
# Validate input directory
if not ($input_root | path exists) {
log error $"Input directory does not exist: ($input_root)"
exit 1
}
# If verifying, run verification instead
if $checksum_config.verify_file != "" {
return verify_checksums $checksum_config
}
# Find files to checksum
let files = find_checksum_files $checksum_config
if ($files | length) == 0 {
log warning "No files found to checksum"
return {
status: "skipped"
reason: "no files found"
files_processed: 0
}
}
log info $"Found ($files | length) files to checksum"
# Generate checksums
let checksum_results = generate_checksums_for_files $files $checksum_config
# Save checksums to file
let save_result = save_checksums $checksum_results $checksum_config
let summary = {
total_files: ($files | length)
successful_files: ($checksum_results | where status == "success" | length)
failed_files: ($checksum_results | where status == "failed" | length)
algorithms_used: $checksum_config.algorithms
output_file: $checksum_config.output_file
checksum_config: $checksum_config
results: $checksum_results
save_result: $save_result
}
if $summary.failed_files > 0 {
log error $"Checksum generation completed with ($summary.failed_files) failures"
exit 1
} else {
log info $"Checksum generation completed successfully - ($summary.successful_files) files processed"
}
return $summary
}
# Find files to generate checksums for
def find_checksum_files [checksum_config: record] -> list {
let find_command = if $checksum_config.recursive {
$"find ($checksum_config.input_dir) -name \"($checksum_config.pattern)\" -type f"
} else {
$"find ($checksum_config.input_dir) -maxdepth 1 -name \"($checksum_config.pattern)\" -type f"
}
let found_files = (bash -c $find_command | lines | where $it != "")
# Filter out checksum files themselves
$found_files | where {|file|
not ($file =~ "checksum" or $file =~ "\.sha256$" or $file =~ "\.md5$" or $file =~ "\.sha512$")
}
}
# Generate checksums for all files
def generate_checksums_for_files [
files: list
checksum_config: record
] -> list {
$files | each {|file|
generate_checksums_for_file $file $checksum_config
}
}
# Generate checksums for a single file
def generate_checksums_for_file [
file: string
checksum_config: record
] -> record {
if $checksum_config.verbose {
log info $"Generating checksums for: ($file)"
}
let start_time = (date now)
let mut checksums = {}
let mut errors = []
# Get file info
let file_info = (ls $file | get 0)
let relative_path = ($file | str replace $checksum_config.input_dir "" | str trim-left "/")
# Generate each requested algorithm
for algorithm in $checksum_config.algorithms {
try {
let checksum = match $algorithm {
"sha256" => { generate_sha256 $file }
"md5" => { generate_md5 $file }
"sha512" => { generate_sha512 $file }
_ => {
$errors = ($errors | append $"Unknown algorithm: ($algorithm)")
""
}
}
if $checksum != "" {
$checksums = ($checksums | insert $algorithm $checksum)
}
} catch {|err|
$errors = ($errors | append $"Failed to generate ($algorithm): ($err.msg)")
}
}
{
file: $file
relative_path: $relative_path
status: (if ($errors | length) > 0 { "failed" } else { "success" })
size: $file_info.size
modified: $file_info.modified
checksums: $checksums
errors: $errors
duration: ((date now) - $start_time)
}
}
# Generate SHA256 checksum
def generate_sha256 [file: string] -> string {
let result = (run-external --redirect-combine "shasum" "-a" "256" $file | complete)
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback to other SHA256 tools
let openssl_result = (run-external --redirect-combine "openssl" "sha256" $file | complete)
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"SHA256\\(.*\\)= " "" | str trim)
} else {
error $"Failed to generate SHA256 for ($file)"
}
}
}
# Generate MD5 checksum
def generate_md5 [file: string] -> string {
let result = (run-external --redirect-combine "md5sum" $file | complete)
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback for macOS
let md5_result = (run-external --redirect-combine "md5" "-r" $file | complete)
if $md5_result.exit_code == 0 {
($md5_result.stdout | split row " " | get 0)
} else {
# Fallback to openssl
let openssl_result = (run-external --redirect-combine "openssl" "md5" $file | complete)
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"MD5\\(.*\\)= " "" | str trim)
} else {
error $"Failed to generate MD5 for ($file)"
}
}
}
}
# Generate SHA512 checksum
def generate_sha512 [file: string] -> string {
let result = (run-external --redirect-combine "shasum" "-a" "512" $file | complete)
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback to openssl
let openssl_result = (run-external --redirect-combine "openssl" "sha512" $file | complete)
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"SHA512\\(.*\\)= " "" | str trim)
} else {
error $"Failed to generate SHA512 for ($file)"
}
}
}
# Save checksums to file
def save_checksums [
checksum_results: list
checksum_config: record
] -> record {
log info $"Saving checksums to: ($checksum_config.output_file)"
let successful_results = ($checksum_results | where status == "success")
try {
match $checksum_config.format {
"standard" => {
save_standard_format $successful_results $checksum_config
}
"json" => {
save_json_format $successful_results $checksum_config
}
"csv" => {
save_csv_format $successful_results $checksum_config
}
_ => {
error $"Unknown format: ($checksum_config.format)"
}
}
{
status: "success"
format: $checksum_config.format
file: $checksum_config.output_file
entries: ($successful_results | length)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Save in standard format (compatible with shasum/md5sum)
def save_standard_format [
results: list
checksum_config: record
] {
let mut output_lines = []
# Add header
$output_lines = ($output_lines | append $"# Checksums generated on (date now)")
$output_lines = ($output_lines | append $"# Algorithms: ($checksum_config.algorithms | str join ', ')")
$output_lines = ($output_lines | append "")
# Add checksums for each algorithm
for algorithm in $checksum_config.algorithms {
$output_lines = ($output_lines | append $"# ($algorithm | str upcase) checksums")
for result in $results {
if ($algorithm in ($result.checksums | columns)) {
let checksum = ($result.checksums | get $algorithm)
$output_lines = ($output_lines | append $"($checksum) ($result.relative_path)")
}
}
$output_lines = ($output_lines | append "")
}
($output_lines | str join "\n") | save $checksum_config.output_file
}
# Save in JSON format
def save_json_format [
results: list
checksum_config: record
] {
let output_data = {
generated_at: (date now)
algorithms: $checksum_config.algorithms
total_files: ($results | length)
files: ($results | each {|result|
{
file: $result.relative_path
size: $result.size
modified: $result.modified
checksums: $result.checksums
}
})
}
$output_data | to json | save $checksum_config.output_file
}
# Save in CSV format
def save_csv_format [
results: list
checksum_config: record
] {
let mut csv_data = []
# Create header
let mut header = ["file", "size", "modified"]
$header = ($header | append $checksum_config.algorithms)
$csv_data = ($csv_data | append ($header | str join ","))
# Add data rows
for result in $results {
let mut row = [$result.relative_path, ($result.size | into string), ($result.modified | format date "%Y-%m-%d %H:%M:%S")]
for algorithm in $checksum_config.algorithms {
let checksum = if ($algorithm in ($result.checksums | columns)) {
($result.checksums | get $algorithm)
} else {
""
}
$row = ($row | append $checksum)
}
$csv_data = ($csv_data | append ($row | str join ","))
}
($csv_data | str join "\n") | save $checksum_config.output_file
}
# Verify checksums from existing file
def verify_checksums [checksum_config: record] -> record {
log info $"Verifying checksums from: ($checksum_config.verify_file)"
if not ($checksum_config.verify_file | path exists) {
log error $"Checksum file does not exist: ($checksum_config.verify_file)"
exit 1
}
let checksum_content = (open $checksum_config.verify_file --raw)
let verification_results = parse_and_verify_checksums $checksum_content $checksum_config
let summary = {
total_verified: ($verification_results | length)
successful_verifications: ($verification_results | where status == "success" | length)
failed_verifications: ($verification_results | where status == "failed" | length)
missing_files: ($verification_results | where status == "missing" | length)
checksum_file: $checksum_config.verify_file
results: $verification_results
}
if ($summary.failed_verifications > 0) or ($summary.missing_files > 0) {
log error $"Checksum verification failed: ($summary.failed_verifications) mismatches, ($summary.missing_files) missing files"
exit 1
} else {
log info $"Checksum verification completed successfully - all ($summary.successful_verifications) files verified"
}
return $summary
}
# Parse and verify checksums from file content
def parse_and_verify_checksums [
content: string
checksum_config: record
] -> list {
let lines = ($content | lines | where {|line|
not ($line | str starts-with "#") and ($line | str trim) != ""
})
$lines | each {|line|
let parts = ($line | split row " " | where $it != "")
if ($parts | length) >= 2 {
let expected_checksum = ($parts | get 0)
let file_path = ($parts | get 1)
let full_path = ($checksum_config.input_dir | path join $file_path)
verify_single_checksum $expected_checksum $full_path $file_path
} else {
{
file: "unknown"
status: "failed"
reason: $"Invalid checksum line: ($line)"
}
}
}
}
# Verify a single checksum
def verify_single_checksum [
expected_checksum: string
full_path: string
relative_path: string
] -> record {
if not ($full_path | path exists) {
return {
file: $relative_path
status: "missing"
expected: $expected_checksum
actual: ""
reason: "file not found"
}
}
try {
# Determine algorithm by checksum length
let algorithm = match ($expected_checksum | str length) {
32 => "md5"
64 => "sha256"
128 => "sha512"
_ => "unknown"
}
let actual_checksum = match $algorithm {
"md5" => { generate_md5 $full_path }
"sha256" => { generate_sha256 $full_path }
"sha512" => { generate_sha512 $full_path }
_ => { "" }
}
if $actual_checksum == $expected_checksum {
{
file: $relative_path
status: "success"
algorithm: $algorithm
expected: $expected_checksum
actual: $actual_checksum
}
} else {
{
file: $relative_path
status: "failed"
algorithm: $algorithm
expected: $expected_checksum
actual: $actual_checksum
reason: "checksum mismatch"
}
}
} catch {|err|
{
file: $relative_path
status: "failed"
expected: $expected_checksum
actual: ""
reason: $err.msg
}
}
}
# Show checksum file info
def "main info" [checksum_file: string = ""] {
if $checksum_file == "" {
# Look for checksum files in current directory
let checksum_files = (find . -maxdepth 1 -name "*checksum*" -o -name "*.sha256" -o -name "*.md5" -o -name "*.sha512" | lines | where $it != "")
return {
current_directory: $env.PWD
checksum_files_found: ($checksum_files | length)
files: $checksum_files
}
}
let checksum_path = ($checksum_file | path expand)
if not ($checksum_path | path exists) {
return { error: "checksum file not found", file: $checksum_path }
}
let content = (open $checksum_path --raw)
let lines = ($content | lines)
let checksum_lines = ($lines | where {|line|
not ($line | str starts-with "#") and ($line | str trim) != ""
})
# Analyze checksum types
let algorithms_detected = ($checksum_lines | each {|line|
let parts = ($line | split row " ")
if ($parts | length) >= 2 {
let checksum = ($parts | get 0)
match ($checksum | str length) {
32 => "md5"
64 => "sha256"
128 => "sha512"
_ => "unknown"
}
} else {
"unknown"
}
} | uniq)
{
file: $checksum_path
total_lines: ($lines | length)
checksum_lines: ($checksum_lines | length)
algorithms_detected: $algorithms_detected
size: (ls $checksum_path | get 0.size)
modified: (ls $checksum_path | get 0.modified)
}
}
# Quick verify command
def "main verify" [
checksum_file: string # Checksum file to verify against
--input-dir: string = "." # Directory containing files to verify
] {
main --verify $checksum_file --input-dir $input_dir
}