#!/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" --output-file: string = "" --algorithms: string = "sha256,md5" --format: string = "standard" --verify: string = "" --recursive --pattern: string = "*" --verbose ] { 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 != "" { verify_checksums $checksum_config return } # 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] { 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 =~ r"checksum" or $file =~ r"\.sha256$" or $file =~ r"\.md5$" or $file =~ r"\.sha512$") } } # Generate checksums for all files def generate_checksums_for_files [ files: list checksum_config: record ] { $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 ] { if $checksum_config.verbose { log info $"Generating checksums for: ($file)" } let start_time = (date now) # Get file info let file_info = (ls $file | get 0) let relative_path = ($file | str replace $checksum_config.input_dir "" | str trim --left "/") # Generate checksums for each algorithm let checksums_list = ($checksum_config.algorithms | each {|algorithm| let result = (do { match $algorithm { "sha256" => { generate_sha256 $file } "md5" => { generate_md5 $file } "sha512" => { generate_sha512 $file } _ => { error make {msg: $"Unknown algorithm: ($algorithm)"} } } } | complete) if $result.exit_code == 0 { {algorithm: $algorithm, checksum: $result.stdout} } else { {algorithm: $algorithm, error: $result.stderr} } }) # Build checksums record from successful results let checksums_record = ($checksums_list | where {|item| $item | has "checksum"} | each {|item| {($item.algorithm): $item.checksum}} | reduce {|item, acc| $acc | merge $item}) # Collect errors from failed results let errors = ($checksums_list | where {|item| $item | has "error"} | each {|item| $item.error}) { file: $file relative_path: $relative_path status: (if ($errors | length) > 0 { "failed" } else { "success" }) size: $file_info.size modified: $file_info.modified checksums: $checksums_record errors: $errors duration: ((date now) - $start_time) } } # Generate SHA256 checksum def generate_sha256 [file: string] { let result = (bash -c $"shasum -a 256 \"($file)\" 2>&1" | complete) if $result.exit_code == 0 { ($result.stdout | split row " " | get 0) } else { # Fallback to other SHA256 tools let openssl_result = (bash -c $"openssl sha256 \"($file)\" 2>&1" | complete) if $openssl_result.exit_code == 0 { ($openssl_result.stdout | str replace $"SHA256\\(.*\\)= " "" | str trim) } else { error make {msg: $"Failed to generate SHA256 for ($file)"} } } } # Generate MD5 checksum def generate_md5 [file: string] { let result = (bash -c $"md5sum \"($file)\" 2>&1" | complete) if $result.exit_code == 0 { ($result.stdout | split row " " | get 0) } else { # Fallback for macOS let md5_result = (bash -c $"md5 -r \"($file)\" 2>&1" | complete) if $md5_result.exit_code == 0 { ($md5_result.stdout | split row " " | get 0) } else { # Fallback to openssl let openssl_result = (bash -c $"openssl md5 \"($file)\" 2>&1" | complete) if $openssl_result.exit_code == 0 { ($openssl_result.stdout | str replace $"MD5\\(.*\\)= " "" | str trim) } else { error make {msg: $"Failed to generate MD5 for ($file)"} } } } } # Generate SHA512 checksum def generate_sha512 [file: string] { let result = (bash -c $"shasum -a 512 \"($file)\" 2>&1" | complete) if $result.exit_code == 0 { ($result.stdout | split row " " | get 0) } else { # Fallback to openssl let openssl_result = (bash -c $"openssl sha512 \"($file)\" 2>&1" | complete) if $openssl_result.exit_code == 0 { ($openssl_result.stdout | str replace $"SHA512\\(.*\\)= " "" | str trim) } else { error make {msg: $"Failed to generate SHA512 for ($file)"} } } } # Save checksums to file def save_checksums [ checksum_results: list checksum_config: record ] { log info $"Saving checksums to: ($checksum_config.output_file)" let successful_results = ($checksum_results | where status == "success") let result = (do { 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 make {msg: $"Unknown format: ($checksum_config.format)"} } } { status: "success" format: $checksum_config.format file: $checksum_config.output_file entries: ($successful_results | length) } } | complete) if $result.exit_code != 0 { { status: "failed" reason: $result.stderr } } else { $result.stdout } } # Save in standard format (compatible with shasum/md5sum) def save_standard_format [ results: list checksum_config: record ] { # Build header let header = [ $"# Checksums generated on (date now)" $"# Algorithms: ($checksum_config.algorithms | str join ', ')" "" ] # Build checksums sections for each algorithm let checksums_sections = ($checksum_config.algorithms | each {|algorithm| ([$"# ($algorithm | str upcase) checksums"] + ($results | where {|result| $algorithm in ($result.checksums | columns)} | each {|result| let checksum = ($result.checksums | get $algorithm) $"($checksum) ($result.relative_path)" }) + [""]) } | flatten) # Combine all lines (($header + $checksums_sections) | 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 ] { # Create header row let header = (["file", "size", "modified"] + $checksum_config.algorithms | str join ",") # Build data rows let data_rows = ($results | each {|result| let base_row = [ $result.relative_path ($result.size | into string) ($result.modified | format date "%Y-%m-%d %H:%M:%S") ] let checksum_row = ($checksum_config.algorithms | each {|algorithm| if ($algorithm in ($result.checksums | columns)) { ($result.checksums | get $algorithm) } else { "" } }) (($base_row + $checksum_row) | str join ",") }) # Combine header and data ([$header] + $data_rows | str join "\n") | save $checksum_config.output_file } # Verify checksums from existing file def verify_checksums [checksum_config: 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 ] { 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 ] { if not ($full_path | path exists) { return { file: $relative_path status: "missing" expected: $expected_checksum actual: "" reason: "file not found" } } # Determine algorithm by checksum length let algorithm = match ($expected_checksum | str length) { 32 => "md5" 64 => "sha256" 128 => "sha512" _ => "unknown" } let result = (do { 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" } } } | complete) if $result.exit_code != 0 { { file: $relative_path status: "failed" expected: $expected_checksum actual: "" reason: $result.stderr } } else { $result.stdout } } # Show checksum file info def "main info" [checksum_file: string = ""] { if $checksum_file == "" { # Look for checksum files in current directory let checksum_files = (bash -c "find . -maxdepth 1 \\( -name '*checksum*' -o -name '*.sha256' -o -name '*.md5' -o -name '*.sha512' \\) -type f" | 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 --input-dir: string = "." ] { main --verify $checksum_file --input-dir $input_dir }