#!/usr/bin/perl -w

######################################################################
###
###   IMPORTANT!
###   Please read the warranty and legal notice 
###   at the end of this file!
###
######################################################################


# To-Do:
# - Format ändern von
#   70473f81de0169849db6c0b540f21397 ./test2/robots.txt 200 865188193
#   auf:
#   70473f81de0169849db6c0b540f21397 200 865188193 ./test2/robots.txt


require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
use slutil;
use English;
use File::Find;


######################################################################
### Voreinstellungen
######################################################################

$version = '1.5.1';
$appname = 'MD5-Check';

$MD5Prog   = 'md5sum';


######################################################################
### Hauptprogramm
######################################################################


&Hilfe if ($#ARGV<0);


######################################################################
### Optionen einlesen
######################################################################

$nice = $FALSE;
OPTION: while ( $ARGV[0] =~ /^-/ )
{
    $_ = shift @ARGV;
    if (/-nice/) { $nice = $TRUE; last OPTION; }

    die "Option '$_' gibt es nicht!\n";
}


######################################################################
### Programm-Start
######################################################################

$dir = KillSlashAtEnd(shift) . $slash;

# Wenn es noch kein MD5.ALT gibt, dann erstmalig erstellen und Ende.
if (! -e "${dir}md5.alt")
{
    erstellen("$dir", "${dir}md5.alt");
    checksum("${dir}md5.alt");
}
else
{
    # Wenn es schon ein MD5.ALT gibt, dann MD5.NEU erstellen und CheckSum
    # drauf loslassen. Dann MD5.NEU nach MD5.ALT umkopieren

    erstellen("$dir", "${dir}md5.neu");

    checksum("${dir}md5.alt", "${dir}md5.neu", "${dir}md5-neu.log");

    open(MD5LOG,">>${dir}md5.log") || die "Kann md5.log nicht finden!\n";
    print MD5LOG &shortdate . "\n"; # Datum anhägen
    open(MD5NEULOG,"${dir}md5-neu.log")
	|| die "Kann md5-neu.log nicht lesen!\n";
    @md5neulog = <MD5NEULOG>; close MD5NEULOG;
    print MD5LOG @md5neulog; # Neues LOG anhängen
    close MD5LOG;
    print @md5neulog;        # Neues LOG auf Bildschirm ausgeben
    unlink("${dir}md5.alt");
    rename("${dir}md5.neu", "${dir}md5.alt")
	|| die "Fehler beim Umbenennen!\n";
}


exit;


######################################################################
### Unterprogramme
######################################################################


sub erstellen
{
    # Liste aller Verzeichnisse erstellen
    # Parameter: Anfangsverzeichnis, Filename
    # Beispiel:  &erstellen("t:\texte", "t:\texte\md5.neu");
    my $dir = shift;
    my $filename = shift;

    # Vorher löschen
    if (-e $filename) { unlink $filename };

    system("find $dir -type f -print0 | xargs -0 $MD5Prog >> $filename");
}


sub checksum
{
    # Parameter: checksum(AltesMD5-File NeuesMD5-File LOG-File)
    # oder:      checksum(MD5-File)
    #
    # Im ersten Fall werden die zwei Files verglichen und evtl. Fehler
    # ins LOG-File ausgegeben, dann wird das neue MD5-File um Größe und
    # Datum ergänzt und geschrieben.
    #
    # Im zweiten Fall wird das MD5-File nur um Größe und Datum ergänzt und
    # geschrieben.
    # MD5 sollte ein File mit folgendem Aufbau generieren:
    # f88551155fc9a2f71f6ec49e7ad61b8d  MD5  CHECKSUM.BAT
    # ab7613658ca4fac3e37c2535d1f2c448  MD5  MDX.EXE
    # oder
    # f88551155fc9a2f71f6ec49e7ad61b8d CHECKSUM.BAT
    # ab7613658ca4fac3e37c2535d1f2c448 MDX.EXE
    # usw...
    # CheckSum wandelt das File in dieses Format:
    # f88551155fc9a2f71f6ec49e7ad61b8d  CHECKSUM.BAT 2850 836075304
    # ab7613658ca4fac3e37c2535d1f2c448  MDX.EXE 13959 731392226

    @argu = @_;

    # Fehlerhafte Parameter abfangen
    unless ( ($#argu==0) || ($#argu==2) )
    { die "checksum() mit falschen Parametern aufgerufen!\n" }

    # Nur ein Parameter => File einlesen und Größe/Datum hinzufügen
    if ( $#argu==0 )
    {
	open (FNeues, "<$argu[0]") 
	    || die "Neues MD5-File kann nicht gelesen werden.\n";
	@neues = <FNeues>;
	close FNeues;
	for ($i=0; $_=$neues[$i], $i<=$#neues; $i++)
	{
	    $neues[$i] = CreateNewMD5Line($_);
	}
	open (FZiel, ">$argu[0]")||die "Ziel kann nicht geschrieben werden.\n";
	print FZiel @neues;
	close FZiel;
	return;
    }

    # Bei Aufruf mit drei Parametern...
    open (FAltes, "<$argu[0]")
	|| die "Altes MD5-File kann nicht gelesen werden.\n";
    @altes = <FAltes>;
    close FAltes;
    open (FNeues, "<$argu[1]")
	|| die "Neues MD5-File kann nicht gelesen werden.\n";
    @neues = <FNeues>;
    close FNeues;
    open (FLog, ">$argu[2]")
	|| die "Log-File kann nicht geschrieben werden.\n";

    # Umwandeln des Alten in ein assoziatives Array:
    $i = 0;
    foreach $_ (@altes)
    {
	($checksum,$name,$size,$date) = SplitMD5Line($_);

	if (! defined $name)
	{print"NAME nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $checksum)
	{print"CHECKSUM nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $size)
	{print"SIZE nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $date)
	{print"DATE nicht initialisiert bei:'$checksum' '$size' '$date'\n"}

	$AssocAltes{"$name"} = "$checksum $size $date\n";
    }

    # Das neue muß erst noch um das Datum und die Filegröße ergänzt werden.
    for ($i=0; $_=$neues[$i], $i<=$#neues; $i++)
    {
	$neues[$i] = CreateNewMD5Line($_);
    }

    # Umwandeln des Neuen in ein assoziatives Array:
    $i = 0;
    foreach $_ (@neues)
    {
	($checksum,$name,$size,$date) = SplitMD5Line($_);

	if (! defined $name)
	{print"NAME nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $checksum)
	{print"CHECKSUM nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $size)
	{print"SIZE nicht initialisiert bei:'$checksum' '$size' '$date'\n"}
	if (! defined $date)
	{print"DATE nicht initialisiert bei:'$checksum' '$size' '$date'\n"}

	$AssocNeues{"$name"} = "$checksum $size $date\n";
    }

    # Jetzt kann man sehr schön das Neue und Alte vergleichen...
    foreach $name (keys %AssocNeues)
    {
	# Wenn es das File noch gibt oder gab, d.h. es könnte ja gelöscht
	# worden sein, oder es könnte ein neues dazugekommen sein.
	if ( defined($AssocAltes{"$name"}) && defined($AssocNeues{"$name"}) )
	{
	    # Aufsplitten der einzelnen Felder:
	    $_ = $AssocAltes{"$name"};
	    ($Achecksum,$Asize,$Adate) = split(/\s+/);
	    $_ = $AssocNeues{"$name"};
	    ($Nchecksum,$Nsize,$Ndate) = split(/\s+/);

	    # 1. Möglichkeit eines Fehlers:
	    # Die Größen sind gleich geblieben und das Datum ist gleich
	    # geblieben, aber die Checksumme hat sich geändert.
	    if(($Asize==$Nsize)&&($Adate==$Ndate)&&($Achecksum ne $Nchecksum))
	    {
	      # fixme Umlaut!
	      print FLog "Im File $name haben sich Bytes veraendert!\n";
	    }

	    # 2. Möglichkeit eines Fehlers:
	    # Datum ist gleich und die Größe hat sich geändert.
	    if ( ($Adate==$Ndate) && ($Asize!=$Nsize) )
	    {
	      # fixme Umlaut!
	      print FLog "Das File $name hat seine Groesse veraendert!\n";
	    }

	}
    }

    # Das neue MD5-File schreiben (Wegen Datum und Filesize.)
    open (FZiel, ">$argu[1]") || die "Ziel kann nicht geschrieben werden.\n";
    print FZiel @neues;
    close FZiel;

    close FLog;

}


sub SplitMD5Line
{
    # Es wird eine Zeile in dieser Art übergeben:
    # "a735183819a33e E:\WOALL\CDIR.DIR 258605 848164328"
    #
    # Return:
    # (MD5, Name, Größe, Datum)
    # nicht vorhandene Werte sind undef.
    #
    $_ = shift;
    /([^ ]+) (.+) (\d+) (-?\d+)/;
    return ($1,$2,$3,$4);
}


sub CreateNewMD5Line
{
    # Es wird eine Zeile in dieser Art übergeben:
    # "a735183819a33e      check.bat"
    #
    # Return:
    # Es wird die komplette neue Zeile zurückgegeben:
    # "a735183819a33e E:\WOALL\CDIR.DIR 258605 848164328"

    $_ = shift;
    s/\s+MD5\s+/ /; # Das "MD5" entfernen;
    /([^ ]+)\s+(.+)/;
    my ($MD5,$name) = ($1,$2);

    # md5sum beginnt jede Zeile mit "\", wenn im Dateinamen ein "\" oder
    # newline enthalten ist! Also für den stat() und zum Abspeichern wieder
    # entfernen!
    if ($MD5 =~ /^\\/)
    {
      $name =~ s/\\\\/\\/g;
    }
    my ($size,$time) = ( (stat($name))[7] , (stat($name))[9] );

    if (! defined $MD5)
    { print "MD5 nicht initialisiert bei: '$MD5' '$name' '$size' '$time'\n" }
    if (! defined $name)
    { print "NAME nicht initialisiert bei: '$MD5' '$name' '$size' '$time'\n" }
    if (! defined $size)
    { print "SIZE nicht initialisiert bei: '$MD5' '$name' '$size' '$time'\n" }
    if (! defined $time)
    { print "TIME nicht initialisiert bei: '$MD5' '$name' '$size' '$time'\n" }

    return "$MD5 $name $size $time\n";
}


sub Kopf
{
  my $head = "$appname $version   -   von Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Zum Prüfen der File-Integrität.

Syntax: md5check [ Optionen ] Verzeichnis

Bsp.:   md5check t:\\texte
        md5check /usr/local/bin
        md5check .

Es werden alle Files in diesem Verzeichnis und allen Unterverzeichnissen
per MD5 getestet. Damit kann man sehr einfach Veränderungen auf einer
Festplatte, die durch Betriebssystemfehler oder defekte Sektoren
entstehen feststellen.
Im letzten Beispiel entstehen relative Verzeichnisangaben.
Dazu muß man natürlich im gewünschten Verzeichnis sein.
(Das ist die beste Methode, denn es dann unabhängig vom absoluten Pfad
oder Mount-Point des Verzeichnisses.)

Optionen:
-nice : Es wird nach jedem Verzeichnis eine kleine Pause gemacht, um
        dem System/User eine Verschnaufpause zu lassen. :)
\n";
exit;
}



######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1997 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.loescher-online.de/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he 
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
#
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
#    it can be downloaded free (without paying for downloading it, except
#    for usage fees that have to be paid anyway). Small copying fees (up to
#    5 DM or 3 $US) may be charged.
#  * Only distribute the whole original package, with all the files included.
#  * This program may not be part of any commercial product or service without
#    the written permission by the author.
#  * If you want to include this program on a CD-ROM and/or book, please send
#    me a free copy of the CD/book (this is not a must, but I would appreciate
#    it very much).
#
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
#
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
#
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
#
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
#
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
#
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
#
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
#
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
#
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
#
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################
