#!/usr/local/bin/ruby
# $Id: digest,v 1.3 2002/05/20 00:44:56 sdalu Exp sdalu $

#
# Copyrigth (C) 2002  Stephane D'Alu
# Code is under GPL v2 (see http://www.gnu.org/licenses/gpl.txt)
#
# WWW: http://www.sdalu.com/software/
#

RCS_ID		= %q$Id: digest,v 1.3 2002/05/20 00:44:56 sdalu Exp sdalu $
RCS_REVISION	= RCS_ID.split[2]
PROGNAME	= File.basename($0)

require 'getoptlong'
require 'digest/md5'
require 'digest/sha1'
require 'digest/rmd160'

# Exit values
EXIT_OK		=  0
EXIT_USAGE	= -1
EXIT_BADCKSUM	=  1
EXIT_MISSING	=  2
EXIT_ERROR	=  3
EXIT_ABORTED	=  4

# Constants
BLOCKSIZE	= 4096


# Arguments default value
quiet		= false
veryquiet	= false
scheme		= "MD5"
check		= false
files		= nil
reverse		= false


# Parse command line
opts = GetoptLong.new(
	[ "--check",	"-c",		GetoptLong::NO_ARGUMENT ],
	[ "--reverse",	"-r",		GetoptLong::NO_ARGUMENT ],
	[ "--scheme",   "-s",           GetoptLong::REQUIRED_ARGUMENT],
	[ "--quiet",	"-q",		GetoptLong::NO_ARGUMENT ],
	[ "--veryquiet","-Q",		GetoptLong::NO_ARGUMENT ],
	[ "--help",	"-h",		GetoptLong::NO_ARGUMENT ],
        [ "--version",	"-v",		GetoptLong::NO_ARGUMENT ] )
opts.quiet = true

begin
    opts.each do |opt, arg|
	case opt
	when "--check"		then check	= true
	when "--reverse"	then reverse	= true
	when "--scheme"		then scheme	= arg
	when "--quiet"		then quiet	= true
	when "--veryquiet"	then veryquiet	= true
	when "--help"		then raise
	when "--version"
	    puts "#{PROGNAME}: RCS version #{RCS_REVISION}"
	    exit EXIT_OK
	end
    end

    raise if check && ARGV.length > 1
    files = ARGV
rescue
    $stderr.print <<EOT
usage: #{PROGNAME} {-h | -v}
       #{PROGNAME} [-r] [-s scheme] [filename...]
       #{PROGNAME} [-cqQ] [filename]
    -c, --check        Check that files match digest value
    -s, --scheme       Select digest scheme (<MD5>/SHA1/RMD160)
    -r, --reverse      Reverse output order
    -q, --quiet        Quiet mode, don't print good file status
    -Q, --veryquiet    Very Quiet mode, doesn't print visual candy
    -h, --help         Show this message
    -v, --version      Display RCS version
EOT
    exit EXIT_USAGE
end


# Prepare for abortion cleanup
trap "SIGINT", proc { exit EXIT_ABORTED }

#
def get_hashing_scheme(scheme) 
    case scheme
    when "MD5"		then Digest::MD5
    when "SHA1"		then Digest::SHA1
    when "RMD160"	then Digest::RMD160
    else raise ArgumentError, "Unknown hash scheme '#{scheme}'"
    end
end


if check then
    badcksum, missing, lineno = 0, 0, 0

    # Select input
    begin 
	io = files.empty? ? $stdin : File::open(files[0])
    rescue Errno::ENOENT => e
	$stderr.puts "#{PROGNAME}: file #{files[0]} doesn't exist."
	exit EXIT_ERROR
    end

    # Check hash values
    io.each { |line|
	lineno     += 1
	begin
	    # Parse line
	    case line 
	    when /^([A-Z][A-Z0-9]*) \((.*)\) = ([a-f0-9]+)$/
		scheme, hash, filename = $1, $3, $2
	    when /^([a-f0-9]{32}) (.*)$/
		scheme, hash, filename = "MD5", $1, $2
	    else
		raise RuntimeError, "Unable to parse content"
	    end
	    
	    # Identify hashing scheme
	    h = get_hashing_scheme(scheme)::new

	    # Hash file
	    missing	+= 1
	    File.open(filename) { |io| 
		while data = io.read(BLOCKSIZE) ; h << data ; end
	    }
	    missing	-= 1
	    
	    # Check
	    if h == hash then
		puts "OK: #{filename}"          unless veryquiet || quiet
	    else
		puts "FAILED: #{filename}"      unless veryquiet
		badcksum += 1
	    end
	    
	rescue RuntimeError, Errno::ENOENT => e
	    $stderr.puts "ERROR: #{e} (line #{lineno})" unless veryquiet
	end
    }
        
    # Print final status
    if badcksum != 0 then
	msg  = "==> #{badcksum} bad file(s)"
	msg += " / #{missing} missing file(s)" if missing != 0
	puts msg
	exit EXIT_BADCKSUM
    elsif missing != 0 then
	puts "==> #{missing} missing file(s)"
	exit EXIT_MISSING
    else
	exit EXIT_OK
    end

else

    # Check hashing scheme
    begin
	scheme.upcase!
	hashing_scheme = get_hashing_scheme(scheme)
    rescue ArgumentError => e
	$stderr.puts "#{PROGNAME}: #{e}"
	exit EXIT_ERROR
    end

    # STDIN or file list
    if files.empty? then
	# Generate hash value from STDIN
	h = hashing_scheme::new
	while data = $stdin.read(BLOCKSIZE) ; h << data ; end
	puts h
    else
	# Generate hash value from supplied file list
	files.each { |file|
	    h = hashing_scheme::new
	    begin
		File.open(file) { |io| 
		    while data = io.read(BLOCKSIZE) ; h << data ; end
		    puts reverse ? "#{h} #{file}" 			\
		                 : "#{scheme} (#{file}) = #{h}"
		}
	    rescue Errno::ENOENT => e
		$stderr.puts "ERROR: #{e}"
	    end
	}
    end

    # Leave
    exit EXIT_OK
end
