542 lines
16 KiB
Plaintext
542 lines
16 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"
|
|
--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
|
|
}
|