530 lines
17 KiB
Plaintext
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
|
|
} |