#!/usr/bin/env perl

# ====================================================================
# check-mime-type.pl: check that every added or property-modified file
# has the svn:mime-type property set and every added or property-modified
# file with a mime-type matching text/* also has svn:eol-style set.
# If any file fails this test the user is sent a verbose error message
# suggesting solutions and the commit is aborted.
#
# Usage: check-mime-type.pl REPOS TXN-NAME
# ====================================================================
# Most of check-mime-type.pl was taken from
# commit-access-control.pl, Revision 9986, 2004-06-14 16:29:22 -0400.
# ====================================================================
# Copyright (c) 2000-2009 CollabNet.  All rights reserved.
# Copyright (c) 2010-2020 Apache Software Foundation (ASF).
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
# ====================================================================

# Turn on warnings the best way depending on the Perl version.
BEGIN {
  if ( $] >= 5.006_000)
    { require warnings; import warnings; }
  else
    { $^W = 1; }
}

use strict;
use Carp;


######################################################################
# Configuration section.

# Toggle: Check files of mime-type text/* for svn:eol-style property.
my $check_text_eol = 1;

# Toggle: Check property-modified files too.
my $check_prop_modified_files = 0;

# Svnlook path.
my $svnlook = "/usr/bin/svnlook";

# Since the path to svnlook depends upon the local installation
# preferences, check that the required program exists to insure that
# the administrator has set up the script properly.
{
  my $ok = 1;
  foreach my $program ($svnlook)
    {
      if (-e $program)
        {
          unless (-x $program)
            {
              warn "$0: required program `$program' is not executable, ",
                   "edit $0.\n";
              $ok = 0;
            }
        }
      else
        {
          warn "$0: required program `$program' does not exist, edit $0.\n";
          $ok = 0;
        }
    }
  exit 1 unless $ok;
}

######################################################################
# Initial setup/command-line handling.

&usage unless @ARGV == 2;

my $repos        = shift;
my $txn          = shift;

unless (-e $repos)
  {
    &usage("$0: repository directory `$repos' does not exist.");
  }
unless (-d $repos)
  {
    &usage("$0: repository directory `$repos' is not a directory.");
  }

# Define two constant subroutines to stand for read-only or read-write
# access to the repository.
sub ACCESS_READ_ONLY  () { 'read-only' }
sub ACCESS_READ_WRITE () { 'read-write' }


######################################################################
# Harvest data using svnlook.

# Change into /tmp so that svnlook diff can create its .svnlook
# directory.
my $tmp_dir = '/tmp';
chdir($tmp_dir)
  or die "$0: cannot chdir `$tmp_dir': $!\n";

# Figure out what files have been added/property-modified using svnlook.
my $regex_files_to_check;
if ($check_prop_modified_files)
  {
    $regex_files_to_check = qr/^(?:A.|.U)  (.*[^\/])$/;
  }
else
  {
    $regex_files_to_check = qr/^A.  (.*[^\/])$/;
  }
my @files_to_check;
foreach my $line (&read_from_process($svnlook, 'changed', $repos, '-t', $txn))
  {
    # Add only files that were added/property-modified to @files_to_check
    if ($line =~ /$regex_files_to_check/)
      {
        push(@files_to_check, $1);
      }
  }

my @errors;
foreach my $path ( @files_to_check )
	{
		my $mime_type;
		my $eol_style;

		# Parse the complete list of property values of the file $path to extract
		# the mime-type and eol-style

		my @output = &read_from_process($svnlook, 'proplist', $repos, '-t',
					$txn, '--verbose', '--', $path);
		my $output_line = 0;

		foreach my $prop (@output)
			{
				if ($prop =~ /^\s*svn:mime-type( : (\S+))?/)
					{
						$mime_type = $2;
						# 1.7.8 (r1416637) onwards changed the format of svnloop proplist --verbose
						# from propname : propvalue format, to values in an indent list on following lines
						if (not $mime_type)
							{
								if ($output_line + 1 >= scalar(@output))
									{
										die "$0: Unexpected EOF reading proplist.\n";
									}
								my $next_line_pval_indented = $output[$output_line + 1];
								if ($next_line_pval_indented =~ /^\s{4}(.*)/)
									{
										$mime_type = $1;
									}
							}
					}
				elsif ($prop =~ /^\s*svn:eol-style( : (\S+))?/)
					{
						$eol_style = $2;
						if (not $eol_style)
							{
								if ($output_line + 1 >= scalar(@output))
									{
										die "$0: Unexpected EOF reading proplist.\n";
									}
								my $next_line_pval_indented = $output[$output_line + 1];
								if ($next_line_pval_indented =~ /^\s{4}(.*)/)
									{
										$eol_style = $1;
									}
							}
					}
				$output_line++;
			}

		# Detect error conditions and add them to @errors
		if (not $mime_type)
			{
				push @errors, "$path : svn:mime-type is not set";
			}
		elsif ($check_text_eol and $mime_type =~ /^text\// and not $eol_style)
			{
				push @errors, "$path : svn:mime-type=$mime_type but svn:eol-style is not set";
			}
	}

# If there are any errors list the problem files and give information
# on how to avoid the problem. Hopefully people will set up auto-props
# and will not see this verbose message more than once.
if (@errors)
  {
    my $addition1 = '';
    my $addition2 = '';
    my $addition3 = '';
    if ($check_prop_modified_files)
      {
        $addition1 = '/property-modified';
      }
    if ($check_text_eol)
      {
        $addition2 = "    In addition text files must have the svn:eol-style property set.\n";
        $addition3 = "    svn propset svn:eol-style native path/of/file\n";
      }
    warn "$0:\n\n",
         join("\n", @errors), "\n\n",
                 <<"EOS";

    Every added$addition1 file must have the svn:mime-type property set.
$addition2
    For binary files try running
    svn propset svn:mime-type application/octet-stream path/of/file

    For text files try
    svn propset svn:mime-type text/plain path/of/file
$addition3
    You may want to consider uncommenting the auto-props section
    in your ~/.subversion/config file. Read the Subversion book
    (https://svnbook.red-bean.com/), Chapter 7, Properties section,
    Automatic Property Setting subsection for more help.
EOS
    exit 1;
  }
else
  {
    exit 0;
  }

sub usage
{
  warn "@_\n" if @_;
  die "usage: $0 REPOS TXN-NAME\n";
}

sub safe_read_from_pipe
{
  unless (@_)
    {
      croak "$0: safe_read_from_pipe passed no arguments.\n";
    }
  print "Running @_\n";
  my $pid = open(SAFE_READ, '-|', @_);
  unless (defined $pid)
    {
      die "$0: cannot fork: $!\n";
    }
  unless ($pid)
    {
      open(STDERR, ">&STDOUT")
        or die "$0: cannot dup STDOUT: $!\n";
      exec(@_)
        or die "$0: cannot exec `@_': $!\n";
    }
  my @output;
  while (<SAFE_READ>)
    {
      chomp;
      push(@output, $_);
    }
  close(SAFE_READ);
  my $result = $?;
  my $exit   = $result >> 8;
  my $signal = $result & 127;
  my $cd     = $result & 128 ? "with core dump" : "";
  if ($signal or $cd)
    {
      warn "$0: pipe from `@_' failed $cd: exit=$exit signal=$signal\n";
    }
  if (wantarray)
    {
      return ($result, @output);
    }
  else
    {
      return $result;
    }
}

sub read_from_process
  {
  unless (@_)
    {
      croak "$0: read_from_process passed no arguments.\n";
    }
  my ($status, @output) = &safe_read_from_pipe(@_);
  if ($status)
    {
      if (@output)
        {
          die "$0: `@_' failed with this output:\n", join("\n", @output), "\n";
        }
      else
        {
          die "$0: `@_' failed with no output.\n";
        }
    }
  else
    {
      return @output;
    }
}
