#!/usr/bin/perl
# compress man and info pages.

use strict;
use warnings;
use Cwd;
use File::Find;
use File::Basename;
use Fcntl ':mode';

my $ext = $ARGV[0] ||= '.gz';
die "Unknown extension $ext" unless $ext =~ /^\.(?:gz|bz2|xz)$/;

my $buildroot = $ENV{RPM_BUILD_ROOT};
die "No build root defined" unless $buildroot;
die "Invalid build root" unless -d $buildroot;
# normalize build root
$buildroot =~ s|/$||;

my $exclude_pattern = $ENV{EXCLUDE_FROM_COMPRESS} ?
    qr/$ENV{EXCLUDE_FROM_COMPRESS}/ : undef;

my @sodirs = map { "$buildroot/$_" } qw(
    usr/man
    usr/X11R6/man
    usr/lib/perl5/man
);
my @mandirs = map { "$buildroot/$_" } qw(
    usr/info
    usr/share/info
    usr/man
    usr/share/man
    usr/X11/man
    usr/lib/perl5/man
);

# Convert man pages from old locations just consisting
# of a single include directive to a symlink
my (@sofiles, @sodests);

sub so_function {
    local $_ = $_;
    my $fn = $_;

    # skip symlinks
    return if -l $fn;
    # skip directories
    return if -d $fn;
    # The -s test is becuase a .so file tends to be small. We don't want
    # to open every man page. 1024 is arbitrary.
    return if -s $fn > 1024;
    # skip excluded files
    return if $exclude_pattern && $File::Find::name =~ $exclude_pattern;

    # Test first line of file for the .so thing.
    open(my $in, $fn);
    my $line = <$in>;
    close($in);
    if ($line =~ m/\.so\s+(.*)/) {
        my $solink = $1;
        # This test is here to prevent links like ... man8/../man8/foo.8
        if (basename($File::Find::dir) eq dirname($solink)) {
            $solink = basename($solink);
        } else {
            $solink = "../$solink";
        }

        push @sofiles, $File::Find::name;
        push @sodests, $solink;
    }
}

foreach my $dir (@sodirs) {
    File::Find::find(\&so_function, $dir) if -e $dir;
}
foreach my $sofile (@sofiles) {
    my $sodest = shift(@sodests);
    unlink $sofile;
    symlink $sodest, $sofile;
}

# find non-compressed info/man pages
my @files;
sub function {
    local $_ = $_;
    my $fn = $_;

    # skip symlinks
    return if -l $fn;
    # skip directories
    return if -d $fn;
    # skip excluded files
    return if $exclude_pattern && $File::Find::name =~ $exclude_pattern;
    # skip compressed files
    return if $fn =~ /\.(?:gz|bz2|xz)$/;
    # skip particular files
    return if $fn eq 'dir' || $fn eq 'whatis';

    push @files, $File::Find::name;
}

foreach my $dir (@mandirs) {
    File::Find::find(\&function, $dir) if -e $dir;
}

# uncompress info/man pages using another format
uncompress_files('.gz', 'gzip') if $ext ne '.gz';
uncompress_files('.bz2', 'bzip2') if $ext ne '.bz2';
uncompress_files('.xz', 'xz') if $ext ne '.xz';

# drop executable bits
foreach my $file (@files) {
    my $mode = (stat($file))[2];
    chmod($mode & ~S_IXUSR & ~S_IXGRP & ~S_IXOTH, $file);
}

if (@files) {

    my @command = $ext eq '.gz'   ? qw(gzip -9f)
                : $ext eq '.bz2'  ? qw(bzip2 -9f)
                : $ext eq '.xz'   ? qw(xz -2ef)
                :                   qw()
                ;
    xargs(\@files, @command)
	or die "Something wrong with the man/info file compression";
}

# Fix up symlinks that were pointing to the uncompressed files.
sub link_function {
    # $_ is already defined by File::Find.
    #
    local $_ = $_;
    my $fn = $_;

    # skip everything but symlinks
    return unless -l $fn;
    # skip non-dangling symlinks
    my $linkval = readlink($fn);
    return if -e "$File::Find::dir/$linkval";

    if (-e "$File::Find::dir/$linkval$ext") {
        unlink $fn;
        symlink "$linkval$ext", "$fn$ext";
    } elsif ($File::Find::dir =~ m|man/|)  {
        # Bad link go on nowhere (any better idea) ?
        unlink $fn;
    }

    return;
}

File::Find::find(\&link_function, $buildroot);

# Run a command that may have a huge number of arguments, like xargs does.
# Pass in a reference to an array containing the arguments, and then other
# parameters that are the command and any parameters that should be passed to
# it each time.
sub xargs {
    my $args = shift;

    # The kernel can accept command lines up to 20k worth of characters.
    my $command_max = 20000;

    # Figure out length of static portion of command.
    my $static_length = 0;
    foreach my $str (@_) {
        $static_length += length($str) + 1;
    }

    my @collect;
    my $length = $static_length;
    foreach my $arg (@$args) {
        if (length($arg) + 1 + $static_length > $command_max) {
            error(qq(This command is greater than the maximum command size allowed by the kernel, and cannot be split up further. What on earth are you doing? "@_ $_"));
        }
        $length += length($arg) + 1;
        if ($length < $command_max) {
            push @collect, $arg;
        } else {
            system(@_, @collect) if @collect > 0;
            @collect = $arg;
            $length = $static_length + length($arg) + 1;
        }
    }
    system(@_, @collect) == 0 if @collect > 0;
}

# uncompress info/man pages with a given extension
sub uncompress_files {
    my ($extension, $command) = @_;

    my @compressed_files;


    foreach my $dir (@mandirs) {
        File::Find::find(
            sub {
                local $_ = $_;
                my $fn = $_;
                # skip symlinks
                return if -l $fn;
                # skip directories
                return if -d $fn;
                # skip excluded files
                return if $exclude_pattern && $File::Find::name =~ $exclude_pattern;
                # skip everything but files with wanted extension
                return if $fn !~ /$extension$/;
                push @compressed_files, $File::Find::name;
            },
            $dir
        ) if -e $dir;
    }

    if (@compressed_files) {
        xargs(\@compressed_files, $command, "-d")
	    or die "Something wrong with the decompression of the $extension man/info file";
        my $length = length($extension);
        push(@files, map { substr($_, 0, -$length) } @compressed_files);
    }
}
