#!/usr/bin/perl
#
# USAGE: specparser.pl file.spec
#
# Output:
#   stdout:
#     list of:
#       <md5><space><url>\n
#     for given specfile.
#     except for lines prefixed with: ERROR:
#   exit status:
#     0 - normal
#     2 - cannot open spec file
#
use strict;
use warnings;

my %no_source;
my $spec;
my $base_spec;
my @spec;
my @sources;
my $sources_file;
my %patchset;

sub print_source($$$);

sub next_spec($)
{
	$spec = shift;
	@spec = ();
	$base_spec = $spec;
	$base_spec =~ s|.*/||;
}

sub error($)
{
	print_once( "ERROR: $base_spec: $_[0]" );
}

sub warning($)
{
	print "ERROR: $base_spec: $_[0]\n";
}

sub trim_spaces($)
{
	my $v = shift;

	$v =~ s/\s+$//;
	$v =~ s/^\s+//;

	return $v;
}

# expand macros in string
sub expand($$) # {{{
{
	my $v = trim_spaces(shift);
	my $macrotree = shift;
	my $cnt = 20;

	while ($v =~ /\%\{([^\}]+)\}/) {
		my $value;
		if (defined $macrotree->{$1}) {
			$value = $macrotree->{$1};
		} else {
			error("undefined macro $1");
			$value = "UNDEFINED";
			return undef;
		}
		$v =~ s/\%\{([^\}]+)\}/$value/;

		return $v if (length $v > 1000 or $cnt-- <= 0)
	}

	while ($v =~ s/\%\(\s*echo\s+([^\|]+?)\s*\|\s*tr\s*(-d|)\s+([^\)]+?)\s*\)/\@\@tr-me\@\@/) {
		my ($what, $d_opt, $how) = ($1, $2, $3);
		my ($from, $to) = ($how, "");
		($from, $to) = ($1, $2)
			if $how =~ /^([^\s]+)\s+([^\s]+)$/;
		if ($d_opt and $to ne "") {
			error("tr -d with second string)");
		} elsif (($from . $to) =~ /^[a-zA-Z0-9\+_\-\.]+$/) {
			if ($d_opt) {
				eval "\$what =~ tr/$from//d;";
			} else {
				eval "\$what =~ tr/$from/$to/;";
			}
		} else {
			error("illegal characters in tr string(s) '$from' '$to'");
		}
		$v =~ s/\@\@tr-me\@\@/$what/;

		return $v if (length $v > 1000 or $cnt-- <= 0)
	}

	error("unexpanded macros in $v")
		if ($v =~ /\%[^0-9]/);

	return $v;
} # }}}

sub preparse_spec($) # {{{
{
	@spec = ("");

	open(F, "< $_[0]") or die("failed opening: " . $_[0]);
	while (<F>) {
		chomp;
		if (/^\s*(\%(description|package|prep|install|pre|post|files)|BuildRoot|URL)/) {
			last;
		} elsif (/^\s*(\%if.*|\%else|\%endif|\%define\s+.*|Version.*|Name.*)\s*$/) {
			$_ = $1;
			if ($spec[$#spec] =~ /\%if/) {
				if (/\%else/) {
					next; # don't include empty %if-%else
				} elsif (/\%endif/) {
					# remove empty %if-%endif
					pop @spec;
					next;
				}
			}
			push @spec, $_;
		} elsif (/^NoSource\s*:\s*(\d+)\s*$/i) {
			$no_source{ "source" . $1 } = 1;
		} elsif (my ($patchset) = /^%patchset_source\s+(.+)$/) {
			use Getopt::Long qw(GetOptionsFromString);
			my ($f, $s);
			my ($ret, $args) = GetOptionsFromString($patchset, 's=s' => \$s, 'f=s' => \$f);
			%patchset = (
				pattern => $f,
				filelist => $s,
				start => $args->[0],
				end => $args->[1],
			);
		}
	}
	close(F);

	shift @spec;
} # }}}

# read in 'sources' file
sub read_sources_file {
	my $filename = $_[0] || $sources_file;
	return () unless $filename and -e $filename;

	my %files;

	open(my $fh, '<', $filename) or die $!;
	while (<$fh>) {
		chomp;
		next unless my ($hash, $filename) = /^([a-f0-9]{32})\s+\*?(.+)$/;
		$files{$filename} = $hash;
	}
	return \%files;
}

sub process_patchset($) {
	my $macros = shift;
	my $checksums;

	return unless %patchset;

	# print all files from sources file
	if ($patchset{filelist}) {
		my $prefix = expand($patchset{pattern}, $macros);
		my $filelist = expand($patchset{filelist}, $macros);
		$checksums = read_sources_file($filelist);
		while (my($file, $hash) = each %$checksums) {
			my $url = $prefix . $file;
			print_source "patchset $file", $hash, $url;
		}
		return;
	}

	# parse sources file sequences
	$checksums = read_sources_file() or return;

	# print out patchset entries which source md5 is present in source file
	my $start = expand($patchset{start}, $macros);
	my $end = expand($patchset{end}, $macros);
	my $pattern = expand($patchset{pattern}, $macros);
	for (my $i = $start; $i <= $end; $i++) {
		my $url = sprintf($pattern, $i);
		my ($basename) = $url =~ m{/([^/]+$)};
		my $hash = $checksums->{$basename} or next;
		print_source "patchset $i", $hash, $url;
	}
}


my $total = 0;

sub cont($$);
sub cont($$) # {{{
{
	my ($spec, $macros) = @_;
	local $_;
	while ($_ = shift @{$spec}) {
		if (0 <= index $_, '%if') { # if, ifarch, ifos

			# split spec parsing
			my @speccopy = @{$spec};
			my %macroscopy = %{$macros};
			cont(\@speccopy, \%macroscopy);

			my $level = 0;
			while ($_ = shift @{$spec}) {
				if ($level <= 0 and ($_ eq '%else' or $_ eq '%endif')) {
					last;
				} elsif (0 <= index $_, '%if') {
					$level++;
				} elsif ($_ eq '%endif') {
					$level--;
				}
			}

			# continue parsing
		} elsif ($_ eq '%else') {

			# %else happens only when %if was interpreted
			# so skip until %endif

			my $level = 0;
			while ($_ = shift @{$spec}) {
				if ($level <= 0 and $_ eq '%endif') {
					last;
				} elsif (0 <= index $_, '%if') {
					$level++;
				} elsif ($_ eq '%endif') {
					$level--;
				}
			}

		} elsif (/^\s*\%(define|global)\s+([^\s]+)\s+([^\s].*?)\s*$/) {
			$macros->{$2} = $3;

		} elsif (/^Version\s*:\s*(.*?)\s*$/i) {
			$macros->{"version"} = $1;
		} elsif (/^Name\s*:\s*(.*?)\s*$/i) {
			$macros->{"name"} = $1;
		} elsif (!/\%endif/) {
			warn "unrecognised line: $_\n";
		}
	}

	# so we have macros now, can parse patchset source
	process_patchset($macros);

	# the end, yuppie !
	foreach my $s (@sources) {
		my $src = expand( $s->[2], $macros );
		if (defined $src) {
			our %tried;
			unless (exists $tried{$src}) {
				print_source( $s->[0], $s->[1], $src );
				$tried{$src} = 1;
			}
		}
	}

	if (++$total > 10000) {
		error("maximum number of bcond posibilities exceeded");
		exit 0;
	}
} # }}}

sub print_once($)
{
	our %printed;
	my $l = shift;
	unless (exists $printed{$l}) {
		print $l . "\n";
		$printed{$l} = 1;
	}
}

sub print_source($$$) # {{{
{
	my ($no, $md5, $s) = @_;

	if ($s =~ /^([a-z0-9A-Z;:\=\?&\@\+\~\.,\-\/\#_]|\%[0-9])+(#\/[a-zA-Z0-9\._-]+)?$/) {
		if ($s =~ /^(ftp|http|https):\/\//) {
			if ($s =~ /\/$/) {
				error("source $no ($s) is directory");
			} else {
				if ($s =~ /:\/\/distfiles\.pld-linux\.org\/src/) {
					$s =~ s|.*/||;
					print_once( "$md5 no-url-copy://$s" );
				} else {
					print_once( "$md5 $s" );
				}
			}
		} else {
			$s =~ s|.*/||;
			print_once( "$md5 no-url://$s");
		}
	} else {
		error("source $no url $s is ill-formatted");
	}
} # }}}

sub add_md5_to_print($) # {{{
{
	open(F, "< $_[0]") or die("failed opening: " . $_[0]);
	my %sourcemap = ();
	while (<F>) {
		chomp;
		if (/^((?:Source|Patch)\d+)\s*:\s*(.*)/i) {
			my $sourceno = lc $1;
			my $source = $2;
			# master.dl is outdated currently
			# $source =~ s/dl.sourceforge.net/master.dl.sourceforge.net/;
			$sourcemap{ $sourceno } = $source;
		} elsif (/^\s*#\s*((?:source|patch)\d+)-md5\s*:\s*([a-f0-9]{32})/i) {
			my $no = lc $1;
			my $md5 = $2;
			if (defined $no_source{$no}) {
				error("both NoSource: $no and md5 given");
			} elsif (defined $sourcemap{ $no} ) {
				my $source = $sourcemap{ $no };
				push @sources, [$no, $md5, $source];
			} else {
				error("source $no not defined (# SourceN-md5: has to be placed just after SourceN:)");
			}
		} elsif (/^\s*BuildRequires:\s*digest\(%((?:SOURCE|PATCH)\d+)\)\s*=\s*([a-f0-9]{32})/i) {
			my $no = lc $1;
			my $md5 = $2;
			if (defined $no_source{ $no }) {
				error("both NoSource: $no and md5 given");
			} elsif (defined $sourcemap{ $no }) {
				my $source = $sourcemap{ $no };
				push @sources, [$no, $md5, $source];
			} else {
				error("source $no not defined (# Source digest has to be placed after SourceN:)");
			}
		}
	}
	close(F);

	# find extra sources from detached checksum file
	my $checksums = read_sources_file();
	my @mapped = map { $_->[0] } @sources;
	delete @sourcemap{@mapped};
	while (my($source, $url) = each %sourcemap) {
		my ($basename) = $url =~ m{/([^/]+$)};
		next unless defined $basename;
		my $hash = $checksums->{$basename} or next;
		push @sources, [$source, $hash, $url];
	}
} # }}}

next_spec(shift);
$sources_file = shift if @ARGV;
preparse_spec($spec);
add_md5_to_print($spec);
cont( \@spec, { "nil" => "" } );

exit(0);

# vim: ts=4:sw=4:fdm=marker
