#!/usr/bin/perl -w
#
# kmsg kernel messages check and print tool.
#
# To check the source code for missing messages the script is called
# with check, the name compiler and the compile parameters
#	kmsg-doc check $(CC) $(c_flags) $<
# To create man pages for the messages the script is called with
#	kmsg-doc print $(CC) $(c_flags) $<
#
# Copyright IBM Corp. 2008
# Author(s):  Martin Schwidefsky <schwidefsky@de.ibm.com>
#	      Michael Holzheu <holzheu@linux.vnet.ibm.com>
#

use Cwd;
use bigint;

my $errors = 0;
my $warnings = 0;
my $srctree = "";
my $objtree = "";
my $kmsg_count = 0;

sub remove_quotes($)
{
    my ($string) = @_;
    my $inside = 0;
    my $slash = 0;
    my $result = "";

    foreach my $str (split(/([\\"])/, $string)) {
        if ($inside && ($str ne "\"" || $slash)) {
            $result .= $str;
        }
        # Check for backslash before quote
        if ($str eq "\"") {
            if (!$slash) {
                $inside = !$inside;
            }
	     $slash = 0;
        } elsif ($str eq "\\") {
            $slash = !$slash;
        } elsif ($str ne "") {
            $slash = 0;
        }
    }
    return $result;
}

sub string_to_bytes($)
{
    my ($string) = @_;
    my %is_escape = ('"', 0x22, '\'', 0x27, 'n', 0x0a, 'r', 0x0d, 'b', 0x08,
		     't', 0x09, 'f', 0x0c, 'a', 0x07, 'v', 0x0b, '?', 0x3f);
    my (@ar, $slash, $len);

    # scan string, interpret backslash escapes and write bytes to @ar
    $len = 0;
    foreach my $ch (split(//, $string)) {
	if ($ch eq '\\') {
	    $slash = !$slash;
	    if (!$slash) {
		$ar[$len] = ord('\\');
		$len++;
	    }
	} elsif ($slash && defined $is_escape{$ch}) {
	    # C99 backslash escapes: \\ \" \' \n \r \b \t \f \a \v \?
	    $ar[$len] = $is_escape{$ch};
	    $len++;
	    $slash = 0;
	} elsif ($slash) {
	    # FIXME: C99 backslash escapes \nnn \xhh
	    die("Unknown backslash escape in message $string.");
	} else {
	    # normal character
	    $ar[$len] = ord($ch);
	    $len++;
	}
    }
    return @ar;
}

sub calc_jhash($)
{
    my ($string) = @_;
    my @ar;
    my ($a, $b, $c, $i, $length, $len);

    @ar = string_to_bytes($string);
    $length = @ar;
    # add dummy elements to @ar to avoid if then else hell
    push @ar, (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    $a = 0x9e3779b9;
    $b = 0x9e3779b9;
    $c = 0;
    $i = 0;
    for ($len = $length + 12; $len >= 12; $len -= 12) {
	if ($len < 24) {
	    # add length for last round
	    $c += $length;
	}
	$a += $ar[$i] + ($ar[$i+1]<<8) + ($ar[$i+2]<<16) + ($ar[$i+3]<<24);
	$b += $ar[$i+4] + ($ar[$i+5]<<8) + ($ar[$i+6]<<16) + ($ar[$i+7]<<24);
	if ($len >= 24) {
	    $c += $ar[$i+8] + ($ar[$i+9]<<8) + ($ar[$i+10]<<16) + ($ar[$i+11]<<24);
	} else {
	    $c += ($ar[$i+8]<<8) + ($ar[$i+9]<<16) + ($ar[$i+10]<<24);
	}
	$a &= 0xffffffff; $b &= 0xffffffff; $c &= 0xffffffff;
	$a -= $b; $a -= $c; $a ^= ($c >> 13); $a &= 0xffffffff;
	$b -= $c; $b -= $a; $b ^= ($a << 8); $b &= 0xffffffff;
	$c -= $a; $c -= $b; $c ^= ($b >> 13); $c &= 0xffffffff;
	$a -= $b; $a -= $c; $a ^= ($c >> 12); $a &= 0xffffffff;
	$b -= $c; $b -= $a; $b ^= ($a << 16); $b &= 0xffffffff;
	$c -= $a; $c -= $b; $c ^= ($b >> 5); $c &= 0xffffffff;
	$a -= $b; $a -= $c; $a ^= ($c >> 3); $a &= 0xffffffff;
	$b -= $c; $b -= $a; $b ^= ($a << 10); $b &= 0xffffffff;
	$c -= $a; $c -= $b; $c ^= ($b >> 15); $c &= 0xffffffff;
	$i += 12;
    }
    return $c;
}

sub add_kmsg_desc($$$$$$)
{
    my ($component, $text, $sev, $argv, $desc, $user) = @_;
    my ($hash, $tag);

    $text = remove_quotes($text);
    $hash = substr(sprintf("%08x", calc_jhash($text)), 2, 6);
    $tag = $component . "." . $hash;

    if ($kmsg_desc{$tag}) {
	if ($text ne $kmsg_desc{$tag}->{'TEXT'}) {
	    warn "Duplicate message with tag $tag\n";
	    warn "  --- $kmsg_desc{$tag}->{'TEXT'}\n";
	    warn "  +++ $text\n";
	} else {
	    warn "Duplicate message description for \"$text\"\n";
	}
	$errors++;
	return;
    }
    $kmsg_desc{$tag}->{'TEXT'} = $text;
    $kmsg_desc{$tag}->{'SEV'} = $sev;
    $kmsg_desc{$tag}->{'ARGV'} = $argv;
    $kmsg_desc{$tag}->{'DESC'} = $desc;
    $kmsg_desc{$tag}->{'USER'} = $user;
}

sub add_kmsg_print($$$$)
{
    my ($component, $sev, $text, $argv) = @_;
    my ($hash, $tag, $count, $parm);

    $text = remove_quotes($text);
    $hash = substr(sprintf("%08x", calc_jhash($text)), 2, 6);
    $tag = $component . "." . $hash;

    # Pretty print severity
    $sev =~ s/"0"/Emerg/;
    $sev =~ s/"1"/Alert/;
    $sev =~ s/"2"/Critical/;
    $sev =~ s/"3"/Error/;
    $sev =~ s/"4"/Warning/;
    $sev =~ s/"5"/Notice/;
    $sev =~ s/"6"/Informational/;
    $sev =~ s/"7"/Debug/;
    $kmsg_print{$kmsg_count}->{'TAG'} = $tag;
    $kmsg_print{$kmsg_count}->{'TEXT'} = $text;
    $kmsg_print{$kmsg_count}->{'SEV'} = $sev;
    $kmsg_print{$kmsg_count}->{'ARGV'} = $argv;
    $kmsg_count += 1;
}

sub process_source_file($$)
{
    my ($component, $file) = @_;
    my $state;
    my ($text, $sev, $argv, $desc, $user);

    if (!open(FD, "$file")) {
	return "";
    }

    $state = 0;
    while (<FD>) {
	chomp;
	# kmsg message component: #define KMSG_COMPONENT "<component>"
	if (/^#define\s+KMSG_COMPONENT\s+\"(.*)\"[^\"]*$/o) {
	    $component = $1;
	}
	if ($state == 0) {
	    # single line kmsg for undocumented messages, format:
	    # /*? Text: "<message>" */
	    if (/^\s*\/\*\?\s*Text:\s*(\".*\")\s*\*\/\s*$/o) {
		add_kmsg_desc($component, $1, "", "", "", "");
	    }
	    # kmsg message start: '/*?'
	    if (/^\s*\/\*\?\s*$/o) {
		$state = 1;
		($text, $sev, $argv, $desc, $user) = ( "", "", "", "", "" );
	    }
	} elsif ($state == 1) {
	    # kmsg message end: ' */'
	    if (/^\s*\*\/\s*/o) {
		add_kmsg_desc($component, $text, $sev, $argv, $desc, $user);
		$state = 0;
	    }
	    # kmsg message text: ' * Text: "<message>"'
	    elsif (/^\s*\*\s*Text:\s*(\".*\")\s*$/o) {
		$text = $1;
	    }
	    # kmsg message severity: ' * Severity: <sev>'
	    elsif (/^\s*\*\s*Severity:\s*(\S*)\s*$/o) {
		$sev = $1;
	    }
	    # kmsg message parameter: ' * Parameter: <argv>'
	    elsif (/^\s*\*\s*Parameter:\s*(\S*)\s*$/o) {
		if (!defined($1)) {
		    $argv = "";
		} else {
		    $argv = $1;
		}
		$state = 2;
	    }
	    # kmsg message description start: ' * Description:'
	    elsif (/^\s*\*\s*Description:\s*(\S*)\s*$/o) {
		if (!defined($1)) {
		    $desc = "";
		} else {
		    $desc = $1;
		}
		$state = 3;
	    }
	    # kmsg has unrecognizable lines
	    else {
		warn "Warning(${file}:$.): Cannot understand $_";
		$warnings++;
		$state = 0;
	    }
	} elsif ($state == 2) {
	    # kmsg message end: ' */'
	    if (/^\s*\*\//o) {
		warn "Warning(${file}:$.): Missing description, skipping message";
		$warnings++;
		$state = 0;
	    }
	    # kmsg message description start: ' * Description:'
	    elsif (/^\s*\*\s*Description:\s*$/o) {
		$desc = $1;
		$state = 3;
	    }
	    # kmsg message parameter line: ' * <argv>'
	    elsif (/^\s*\*(.*)$/o) {
		$argv .= "\n" . $1;
	    } else {
		warn "Warning(${file}:$.): Cannot understand $_";
		$warnings++;
		$state = 0;
	    }
	} elsif ($state == 3) {
	    # kmsg message end: ' */'
	    if (/^\s*\*\/\s*/o) {
		add_kmsg_desc($component, $text, $sev, $argv, $desc, $user);
		$state = 0;
	    }
	    # kmsg message description start: ' * User action:'
	    elsif (/^\s*\*\s*User action:\s*$/o) {
		$user = $1;
		$state = 4;
	    }
	    # kmsg message description line: ' * <text>'
	    elsif (/^\s*\*\s*(.*)$/o) {
		$desc .= "\n" . $1;
	    } else {
		warn "Warning(${file}:$.): Cannot understand $_";
		$warnings++;
		$state = 0;
	    }
	} elsif ($state == 4) {
	    # kmsg message end: ' */'
	    if (/^\s*\*\/\s*/o) {
		add_kmsg_desc($component, $text, $sev, $argv, $desc, $user);
		$state = 0;
	    }
	    # kmsg message user action line: ' * <text>'
	    elsif (/^\s*\*\s*(.*)$/o) {
		$user .= "\n" . $1;
	    } else {
		warn "Warning(${file}:$.): Cannot understand $_";
		$warnings++;
		$state = 0;
	    }
	}
    }
    return $component;
}

sub process_cpp_file($$$$)
{
    my ($cc, $options, $file, $component) = @_;

    open(FD, "$cc $gcc_options|") or die ("Preprocessing failed.");

    while (<FD>) {
	chomp;
	if (/.*__KMSG_PRINT\(\s*(\S*)\s*(\S*)\s*_FMT_(.*)_ARGS_\s*(.*)?_END_\s*\)/o) {
	    if ($component ne "") {
		add_kmsg_print($component, $2, $3, $4);
	    } else {
		warn "Error(${file}:$.): kmsg without component\n";
		$errors++;
	    }
	} elsif (/.*__KMSG_DEV\(\s*(\S*)\s*(\S*)\s*_FMT_(.*)_ARGS_\s*(.*)?_END_\s*\)/o) {
	    if ($component ne "") {
		add_kmsg_print($component, $2, "\"%s: \"" . $3, $4);
	    } else {
		warn "Error(${file}:$.): kmsg without component\n";
		$errors++;
	    }
	}
    }
}

sub check_messages($)
{
    my $component = "@_";
    my $failed = 0;

    for ($i = 0; $i < $kmsg_count; $i++) {
	$tag = $kmsg_print{$i}->{'TAG'};
	if (!defined($kmsg_desc{$tag})) {
	    add_kmsg_desc($component,
			  "\"" . $kmsg_print{$i}->{'TEXT'} . "\"",
			  $kmsg_print{$i}->{'SEV'},
			  $kmsg_print{$i}->{'ARGV'},
			  "Please insert description here",
			  "What is the user supposed to do");
	    $kmsg_desc{$tag}->{'CHECK'} = 1;
	    $failed = 1;
	    warn "$component: Missing description for: ".
		 $kmsg_print{$i}->{'TEXT'}."\n";
	    $errors++;
	    next;
	}
	if ($kmsg_desc{$tag}->{'SEV'} ne "" &&
	    $kmsg_desc{$tag}->{'SEV'} ne $kmsg_print{$i}->{'SEV'}) {
	    warn "Message severity mismatch for \"$kmsg_print{$i}->{'TEXT'}\"\n";
	    warn "  --- $kmsg_desc{$tag}->{'SEV'}\n";
	    warn "  +++ $kmsg_print{$i}->{'SEV'}\n";
	}
    }
    return $failed;
}

sub print_templates()
{
    print "Templates for missing messages:\n";
    foreach $tag ( sort { $kmsg_desc{$a} <=> $kmsg_desc{$b} } keys %kmsg_desc ) {
	if (!defined($kmsg_desc{$tag}->{'CHECK'})) {
	    next;
	}
	print "/*?\n";
	print " * Text: \"$kmsg_desc{$tag}->{'TEXT'}\"\n";
	print " * Severity: $kmsg_desc{$tag}->{'SEV'}\n";
	$argv = $kmsg_desc{$tag}->{'ARGV'};
	if ($argv ne "") {
	    print " * Parameter:\n";
	    @parms = split(/\s*,\s*/,$kmsg_desc{$tag}->{'ARGV'});
	    $count = 0;
	    foreach $parm (@parms) {
		$count += 1;
		if (!($parm eq "")) {
		    print " *   \@$count: $parm\n";
		}
	    }
	}
	print " * Description:\n";
	print " * $kmsg_desc{$tag}->{'DESC'}\n";
	print " * User action:\n";
	print " * $kmsg_desc{$tag}->{'USER'}\n";
	print " */\n\n";
    }
}

sub write_man_pages()
{
    my ($i, $file);

    for ($i = 0; $i < $kmsg_count; $i++) {
	$tag = $kmsg_print{$i}->{'TAG'};
	if (!defined($kmsg_desc{$tag}) ||
	    defined($kmsg_desc{$tag}->{'CHECK'}) ||
	    $kmsg_desc{$tag}->{'DESC'} eq "") {
	    next;
	}
	$file = $objtree . "man/" . $tag . ".9";
	if (!open(WR, ">$file")) {
	    warn "Error: Cannot open file $file\n";
	    $errors++;
	    return;
	}
	print WR ".TH \"$tag\" 9 \"Linux Messages\" LINUX\n";
	print WR ".SH Message\n";
	print WR $tag . ": " . $kmsg_desc{$tag}->{'TEXT'} . "\n";
	print WR ".SH Severity\n";
	print WR "$kmsg_desc{$tag}->{'SEV'}\n";
	$argv = $kmsg_desc{$tag}->{'ARGV'};
	if ($argv ne "") {
	    print WR ".SH Parameters\n";
	    @parms = split(/\s*\n\s*/,$kmsg_desc{$tag}->{'ARGV'});
	    foreach $parm (@parms) {
		$parm =~ s/^\s*(.*)\s*$/$1/;
		if (!($parm eq "")) {
		    print WR "$parm\n\n";
		}
	    }
	}
	print WR ".SH Description";
	print WR "$kmsg_desc{$tag}->{'DESC'}\n";
	$user = $kmsg_desc{$tag}->{'USER'};
	if ($user ne "") {
	    print WR ".SH User action";
	    print WR "$user\n";
	}
    }
}

if (defined($ENV{'srctree'})) {
    $srctree = "$ENV{'srctree'}" . "/";
} else {
    $srctree = getcwd;
}

if (defined($ENV{'objtree'})) {
    $objtree = "$ENV{'objtree'}" . "/";
} else {
    $objtree = getcwd;
}

if (defined($ENV{'SRCARCH'})) {
    $srcarch = "$ENV{'SRCARCH'}" . "/";
} else {
    print "kmsg-doc called without a valid \$SRCARCH\n";
    exit 1;
}

$option = shift;

$cc = shift;
$gcc_options = "-E -D __KMSG_CHECKER ";
foreach $tmp (@ARGV) {
    $tmp =~ s/\(/\\\(/;
    $tmp =~ s/\)/\\\)/;
    $gcc_options .= " $tmp";
    $filename = $tmp;
}

$component = process_source_file("", $filename);
if ($component ne "") {
    process_source_file($component, $srctree . "Documentation/kmsg/" .
			$srcarch . $component);
    process_source_file($component, $srctree . "Documentation/kmsg/" .
			$component);
}

process_cpp_file($cc, $gcc_options, $filename, $component);
if ($option eq "check") {
    if (check_messages($component)) {
	print_templates();
    }
} elsif ($option eq "print") {
    write_man_pages();
}

exit($errors);
