#!/usr/bin/perl
package IkiWiki::Plugin::filecheck;

use warnings;
use strict;
use IkiWiki 3.00;

my %units=(		# size in bytes
	B		=> 1,
	byte		=> 1,
	KB		=> 2 ** 10,
	kilobyte 	=> 2 ** 10,
	K		=> 2 ** 10,
	KB		=> 2 ** 10,
	kilobyte 	=> 2 ** 10,
	M		=> 2 ** 20,
	MB		=> 2 ** 20,
	megabyte	=> 2 ** 20,
	G		=> 2 ** 30,
	GB		=> 2 ** 30,
	gigabyte	=> 2 ** 30,
	T		=> 2 ** 40,
	TB		=> 2 ** 40,
	terabyte	=> 2 ** 40,
	P		=> 2 ** 50,
	PB		=> 2 ** 50,
	petabyte	=> 2 ** 50,
	E		=> 2 ** 60,
	EB		=> 2 ** 60,
	exabyte		=> 2 ** 60,
	Z		=> 2 ** 70,
	ZB		=> 2 ** 70,
	zettabyte	=> 2 ** 70,
	Y		=> 2 ** 80,
	YB		=> 2 ** 80,
	yottabyte	=> 2 ** 80,
	# ikiwiki, if you find you need larger data quantities, either modify
	# yourself to add them, or travel back in time to 2008 and kill me.
	#   -- Joey
);

sub import {
	hook(type => "getsetup", id => "filecheck",  call => \&getsetup);
}

sub getsetup () {
	return
		plugin => {
			safe => 1,
			rebuild => undef,
		},
}

sub parsesize ($) {
	my $size=shift;

	no warnings;
	my $base=$size+0; # force to number
	use warnings;
	foreach my $unit (sort keys %units) {
		if ($size=~/[0-9\s]\Q$unit\E$/i) {
			return $base * $units{$unit};
		}
	}
	return $base;
}

# This is provided for other plugins that want to convert back the other way.
sub humansize ($) {
	my $size=shift;

	foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
		if ($size / $units{$unit} > 0.25) {
			return (int($size / $units{$unit} * 10)/10).$unit;
		}
	}
	return $size; # near zero, or negative
}

package IkiWiki::PageSpec;

sub match_maxsize ($$;@) {
	my $page=shift;
	my $maxsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
	if ($@) {
		return IkiWiki::ErrorReason->new("unable to parse maxsize (or number too large)");
	}

	my %params=@_;
	my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
	if (! defined $file) {
		return IkiWiki::ErrorReason->new("file does not exist");
	}

	if (-s $file > $maxsize) {
		return IkiWiki::FailReason->new("file too large (".(-s $file)." >  $maxsize)");
	}
	else {
		return IkiWiki::SuccessReason->new("file not too large");
	}
}

sub match_minsize ($$;@) {
	my $page=shift;
	my $minsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
	if ($@) {
		return IkiWiki::ErrorReason->new("unable to parse minsize (or number too large)");
	}

	my %params=@_;
	my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
	if (! defined $file) {
		return IkiWiki::ErrorReason->new("file does not exist");
	}

	if (-s $file < $minsize) {
		return IkiWiki::FailReason->new("file too small");
	}
	else {
		return IkiWiki::SuccessReason->new("file not too small");
	}
}

sub match_mimetype ($$;@) {
	my $page=shift;
	my $wanted=shift;

	my %params=@_;
	my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
	if (! defined $file) {
		return IkiWiki::ErrorReason->new("file does not exist");
	}

	# Get the mime type.
	#
	# First, try File::Mimeinfo. This is fast, but doesn't recognise
	# all files.
	eval q{use File::MimeInfo::Magic};
	my $mimeinfo_ok=! $@;
	my $mimetype;
	if ($mimeinfo_ok) {
		$mimetype=File::MimeInfo::Magic::magic($file);
	}

	# Fall back to using file, which has a more complete
	# magic database.
	if (! defined $mimetype) {
		open(my $file_h, "-|", "file", "-bi", $file);
		$mimetype=<$file_h>;
		chomp $mimetype;
		close $file_h;
	}
	if (! defined $mimetype) {
		# Fall back to default value.
		$mimetype=File::MimeInfo::Magic::default($file)
			if $mimeinfo_ok;
		if (! defined $mimetype) {
			$mimetype="unknown";
		}
	}
	# Ignore any parameters, we only want the type itself
	$mimetype =~ s/;.*//;

	my $regexp=IkiWiki::glob2re($wanted);
	if ($mimetype!~$regexp) {
		return IkiWiki::FailReason->new("file MIME type is $mimetype, not $wanted");
	}
	else {
		return IkiWiki::SuccessReason->new("file MIME type is $mimetype");
	}
}

sub match_virusfree ($$;@) {
	my $page=shift;
	my $wanted=shift;

	my %params=@_;
	my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
	if (! defined $file) {
		return IkiWiki::ErrorReason->new("file does not exist");
	}

	if (! exists $IkiWiki::config{virus_checker} ||
	    ! length $IkiWiki::config{virus_checker}) {
		return IkiWiki::ErrorReason->new("no virus_checker configured");
	}

	# The file needs to be fed into the virus checker on stdin,
	# because the file is not world-readable, and if clamdscan is
	# used, clamd would fail to read it.
	eval q{use IPC::Open2};
	error($@) if $@;
	open (IN, "<", $file) || return IkiWiki::ErrorReason->new("failed to read file");
	binmode(IN);
	my $sigpipe=0;
	$SIG{PIPE} = sub { $sigpipe=1 };
	my $pid=open2(\*CHECKER_OUT, "<&IN", $IkiWiki::config{virus_checker}); 
	my $reason=<CHECKER_OUT>;
	chomp $reason;
	1 while (<CHECKER_OUT>);
	close(CHECKER_OUT);
	waitpid $pid, 0;
	$SIG{PIPE}="DEFAULT";
	if ($sigpipe || $?) {
		if (! length $reason) {
			$reason="virus checker $IkiWiki::config{virus_checker}; failed with no output";
		}
		return IkiWiki::FailReason->new("file seems to contain a virus ($reason)");
	}
	else {
		return IkiWiki::SuccessReason->new("file seems virusfree ($reason)");
	}
}

sub match_ispage ($$;@) {
	my $filename=shift;

	if (defined IkiWiki::pagetype($filename)) {
		return IkiWiki::SuccessReason->new("file is a wiki page");
	}
	else {
		return IkiWiki::FailReason->new("file is not a wiki page");
	}
}
