#!/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 }