#!/bin/bash
#
# make a changes file for a package upload -- andys version 3.0
#

#set -x

# .changes file format version
FORMAT=1.5

WARNINGS=0

# file holders
TAR_FILE=""
DIFF_FILE=""
DEB_FILES=""
UNK_FILES=""

# output file
OUTPUT=""

# Stuff found in DEB files
ARCH=""
VERSION=""
BINARY=""
MAINTAINER=""
SOURCE=""
SECTION=""
SECTIONS=""

# useful constants
SUCCESS=0
FAILURE=1
DATE=`date -u "+%d %b %y %H:%M UT"`
PROG=`basename $0`
PRIORITIES="Low Routine High Urgent"

# command line options flags
EOPTION=FALSE # extended descriptions
NOPTION=FALSE # no edit
POPTION=FALSE # pgp sign
WOPTION=FALSE # make many errors warnings
OOPTION=TRUE # old style filenames

# overrides
O_PGP=""             # signer for pgp
O_DIST="unstable" # default distribution
O_PRI="Low"          # default priority
O_CHA_S=""           # changes string
O_CHA_E=""           # get changes from file
O_SECT=""
O_DPRI=""
SPEC_FILES=""        # special files (ie leave alone)

function main
{
  for ARG in "$@"
  do
    case $ARG in
      -e)  shift 1; EOPTION=TRUE;;
      -n)  shift 1; NOPTION=TRUE;;
      -p)  shift 1; POPTION=TRUE;;
      -w)  shift 1; WOPTION=TRUE;;
      maint=*) MAINTAINER=`echo $ARG | cut -d= -f2`; shift 1;;
      pgp=*) O_PGP=`echo $ARG | cut -d= -f2`; shift 1;;
      dist=*) O_DIST=`echo $ARG | cut -d= -f2`; shift 1;;
      pri=*) O_PRI=`echo $ARG | cut -d= -f2`; shift 1;;
      cs=*) O_CHA_S=`echo $ARG | cut -d= -f2`; shift 1;;
      ce=*) O_CHA_E=`echo $ARG | cut -d= -f2`; shift 1;;
      arch=*) O_ARCH=`echo $ARG | cut -d= -f2`; shift 1;;
      src=*) O_SRC=`echo $ARG | cut -d= -f2`; shift 1;;
      bin=*) O_BIN=`echo $ARG | cut -d= -f2`; shift 1;;
      ver=*) O_VER=`echo $ARG | cut -d= -f2`; shift 1;;
      sf=*)  SPEC_FILES="${SPEC_FILES} "`echo $ARG | cut -d= -f2`; shift 1;;
      maint=*) MAINTAINER=`echo $ARG | cut -d= -f2`; shift 1;;
      sect=*) O_SECT=`echo $ARG | cut -d= -f2`; shift 1;;
      -\?) do_usage; exit $SUCCESS;;
      -*)  stderr "ERROR: $ARG not supported"; exit $FAILURE;;
    esac
  done

  ARCH="$O_ARCH"
  SOURCE="$O_SRC"
  BINARY="$O_BIN"
  VERSION="$O_VER"
  SECTION="$O_SECT"
  
  do_files $*
  if [ -z $SOURCE ]
  then
    error "ERROR: No source package name found - cannot create changes file."
  else
    do_buildit
    if [ $WARNINGS -gt 0 ]
    then
      echo "$WARNINGS warning(s) - hit return to continue"
      read
    fi
    [ "$NOPTION" = FALSE ] && do_edit $OUTFILE
    do_syntax $OUTFILE
    [ "$POPTION" = FALSE ] && do_pgp $OUTFILE
  fi
  echo "Changes file created as: $OUTFILE"
  return 0
}

function do_pgp
{
  if ! type pgp >/dev/null 2>&1
  then
    stderr "ERROR:" "pgp(1) not found"
    exit $FAILURE
  fi

  # prefer the override mainainter
  EMAIL=$O_PGP
  if [ -z "$EMAIL" ]
  then
    EMAIL=$MAINTAINER
  fi
  if [ -z "$EMAIL" ]
  then
    stderr "ERROR: cannot find Maintainer to use for pgp signing"
    exit $FAILURE
  fi
  if ! pgp -u "$EMAIL" -sta $OUTFILE 
  then
    stderr "ERROR:" "pgp(1) failed to sign $OUTFILE"
    exit $FAILURE
  else
    mv $OUTFILE.asc $OUTFILE
  fi
}

function do_edit
{
  FILE=$1
  MY_EDITORS="vi emacs pico joe ee ae ed"
  if [ ! -z "$EDITOR" ]
  then
    MY_EDITOR=$EDITOR
  else
    if [ ! -z "$VISUAL" ]
    then
      MY_EDITOR=$VISUAL
    else
      for MY_EDITOR in $MY_EDITORS
      do
	type $MY_EDITOR >/dev/null 2>&1 && break
      done
    fi
  fi
  if ! type $MY_EDITOR >/dev/null 2>&1
  then
    stderr "ERROR:"  "Unable to find editor $MY_EDITOR"
    exit $FAILURE
  fi
  $MY_EDITOR $FILE
}

function do_buildit
{
  if [ -z "${VERSION}" ]
  then
    OUTFILE="${SOURCE}.changes"
  else
    OUTFILE="${SOURCE}-${VERSION}.changes"
  fi
  >$OUTFILE
  echo "Date: ${DATE}" >> $OUTFILE
  echo "Format: ${FORMAT}" >> $OUTFILE
  echo "Distribution: ${O_DIST}" >> $OUTFILE
  echo "Priority: ${O_PRI}" >> $OUTFILE
  if [ -z "${MAINTAINER}" ]
  then
    echo "Maintainer: ${O_PGP}" >> $OUTFILE
  else
    echo "Maintainer: ${MAINTAINER}" >> $OUTFILE
  fi
  test ! -z "$SOURCE" && echo "Source: ${SOURCE}" >> $OUTFILE
  test ! -z "$VERSION" && echo "Version: $VERSION" >> $OUTFILE
  test ! -z "$BINARY" && echo "Binary: ${BINARY}" >> $OUTFILE
  test ! -z "$ARCH" && echo "Architecture: $ARCH" >> $OUTFILE
  if [ -z "$DEB_FILES" ]
  then
    echo "Description: no description provided" >> $OUTFILE
  else
    echo "Description: " >>$OUTFILE
    for FILE in $DEB_FILES
    do
      case $FILE in
	*.deb)
	       THISPKG=`dpkg-deb --field $FILE package`
	       echo -n " $THISPKG: " >>$OUTFILE
	       dpkg-deb --field $FILE description | head -1 >>$OUTFILE
	       if [ "$EOPTION" = "TRUE" ]
	       then
		 dpkg-deb --field $FILE description | tail +2 \
		  | sed -e 's/^/ - /'>>$OUTFILE
	       fi
	       ;;
      esac
    done
  fi

  if [ -n "$DEB_FILES" -o -n "$UNK_FILES" -o -n "$TAR_FILE" -o -n "$DIFF_FILE" ]
  then
    if [ -z "$O_CHA_S" -a -z "$O_CHA_E" ]
    then
      O_CHA_S="no changes info provided"
    fi
    echo "Changes: $O_CHA_S" >>$OUTFILE
    if [ ! -z "$O_CHA_E" ]
    then
      sed -e 's/^/ /; s/^[ 	]*$/ ./' <$O_CHA_E | col -x >>$OUTFILE
    fi
  fi

  # output files
  if [ -n "${TAR_FILE}${DIFF_FILE}${UNK_FILES}" ]
  then
    # figure out section
    if [ -z "$SECTION" ]
    then
      if (echo "$SECTIONS" | grep "[a-z0-9] " > /dev/null)
      then
	SECTION=`echo "$SECTIONS" | cut -d" " -f1`
	echo    "WARNING: more than one section field found: ${SECTIONS}"
	warning "         using $SECTION"
      else
	SECTION="${SECTIONS}"
      fi
    fi
    echo "Files:" >>$OUTFILE
    for FILE in $TAR_FILE $DIFF_FILE $UNK_FILES
    do
      out_file $FILE $SECTION -
    done
    for FILE in $DEB_FILES
    do
      case $FILE in
	s=*) O_SECT=`echo $FILE | cut -d= -f2`;;
	p=*) O_DPRI=`echo $FILE | cut -d= -f2`;;
	*.deb)
	       SECT=`dpkg-deb --field $FILE section`
	       if [ -z "$SECT" ]
	       then
		 if [ -z "$O_SECT" ]
		 then
  		   warning "WARNING: no section defined for ${FILE}, using misc"
		     SECT=misc
		 else
		   SECT="$O_SECT"
		 fi
	       fi
	       DPRI=`dpkg-deb --field $FILE priority`
	       if [ -z "$DPRI" ]
	       then
		 if [ -z "$O_DPRI" ]
		 then
  		   warning "WARNING: no priority defined for ${FILE}, using extra"
		     DPRI=extra
		 else
		   DPRI="$O_DPRI"
		 fi
	       fi
	       out_file $FILE $SECT $DPRI
	       ;;
	esac
    done
    for FILE in $SPEC_FILES
    do
      out_file $FILE byhand -
    done
  fi
}

function do_debs
{
  for DEB in $DEB_FILES
  do
    case $DEB in
      s=*) ;;
      p=*) ;;
      *.deb)
	     # get fields from the deb file and check them

	     # Binary
	     T_BINARY=`dpkg-deb --field $DEB package`
	     BINARY="$BINARY $T_BINARY"
	     
	     # Architecture
	     T_ARCH=`dpkg-deb --field $DEB architecture`
	     if [ -z "$T_ARCH" ]
	     then
	       T_ARCH=i386
	     fi
	     (echo "$ARCH" | fgrep "$T_ARCH" > /dev/null) || ARCH="$ARCH $T_ARCH"

	     # Section
	     T_SECTION=`dpkg-deb --field $DEB section`
	     if [ -n "$T_SECTION" ]
	     then
	       (echo "$SECTIONS" | fgrep "$T_SECTION" > /dev/null) || \
		SECTIONS="$SECTIONS $T_SECTION"
	     fi
    
	     # Source - fix ????
	     T_SRC=`dpkg-deb --field $DEB source`
	     T_SRCVER=""
	     if [ -z "$T_SRC" ]
	     then
	       T_SOURCE=$T_BINARY
	     else
	       T_SOURCE=`echo $T_SRC | awk -- '{print $1}'`
	       T_SRCVER=`echo $T_SRC | awk -- '{print $2}'`
	       if [ -n "$T_SRCVER" ]
	       then
		 if [ -z "$VERSION" ]
		 then
		   VERSION=$T_SRCVER
		 else
		   if [ "$VERSION" != "$T_SRCVER" ]
		   then
		     stderr "ERROR: Version mismatch between Source: field and previous value"
		     error  "       got $T_SRC in $DEB"
		   fi
		 fi
	       fi
	     fi
    
	     if [ -z "$SOURCE" ]
	     then
	       SOURCE=$T_SOURCE
	     else
	       if [ "$SOURCE" != "$T_SOURCE" ]
	       then
		 stderr "ERROR: Source field mismatch in deb file $DEB"
		 error  "       got $SOURCE and $T_SOURCE"
	       fi
	     fi

	     # Revision
	     T_REVISION=`dpkg-deb --field $DEB package_revision`
    
	     # Version
	     T_VERSION=`dpkg-deb --field $DEB version`
	     if [ -n "$T_REVISION" ]
	     then
	       T_VERSION="${T_VERSION}-${T_REVISION}"
	     fi
	     if [ -z "$T_SRCVER" -a -z "$VERSION" ]
	     then
	       VERSION=$T_VERSION
	     else
	       if [ "$VERSION" != "$T_VERSION" ]
	       then
		 stderr "ERROR: Version field mismatch in deb file $DEB"
		 error  "       got $VERSION and $T_VERSION"
	       fi
	     fi

	     # Maintainer
	     T_MAINTAINER=`dpkg-deb --field $DEB maintainer`
	     if [ -z "$MAINTAINER" ]
	     then
	       MAINTAINER=$T_MAINTAINER
	     else
	       if [ "$MAINTAINER" != "$T_MAINTAINER" ]
	       then
		 stderr "ERROR: Maintainer field mismatch in deb file $DEB"
		 error  "       got $MAINTAINER and $T_MAINTAINER"
	       fi
	     fi

	     # check filename matches contents
	     FN=`basename "${DEB}"`
	     if [ "$FN" != "${T_BINARY}-${T_VERSION}.${T_ARCH}.deb" ]
	     then
	       # special case for i386 arch
	       if [ "$T_ARCH" = "i386" ]
	       then
		 if [ "$FN" != "${T_BINARY}.${T_VERSION}.deb" ]
		 then
		   stderr "ERROR: Deb filename dosn't match contents: $FN"
		   error  "       expected ${T_BINARY}-${T_VERSION}.${T_ARCH}.deb"
		 else
		   warning "WARNING: missing ARCHITECTURE component in file name: $FN"
		 fi
	       else
		 stderr "ERROR: Deb filename dosn't match contents: $FN"
		 error  "       expected ${T_BINARY}-${T_VERSION}.${T_ARCH}.deb"
	       fi
	     else
	       echo "Deb file ok: $FN"
	     fi
	     ;;
      *)
	 stderr "Something very wrong with dchanges"
	 exit 2
    esac
  done
}

# out_file <filename> <section> <pri>
function out_file
{
  echo -n " " >>$OUTFILE
  # md5sum
  echo -n `md5sum $1 | cut -d' ' -f1` >>$OUTFILE
  # size
  echo -n " " `wc -c $1 | sed -e 's/[^0-9]*\([0-9]*\).*/\1/'` >>$OUTFILE
  # section
  echo -n " " $2 >> $OUTFILE
  # priority
  echo -n " " $3 >> $OUTFILE
  # filename
  echo -n " " `basename $1` >> $OUTFILE
  echo >> $OUTFILE
}

function do_usage
{
  echo "Usage: $PROG [options] [overrides] [filename|deboverride]+"
  echo "Options:"
  echo "  -e         # Include extended package descriptions"
  echo "  -n         # No edit of the changes file"
  echo "  -p         # Don't sign changes file with pgp(1)"
  echo "  -?         # Print this message"
  echo "Overrides:"
  echo "  cs=string  # place string in Changes field summary"
  echo "  ce=file    # copy file to Changes field extension"
  echo "  pri=string # place string in Priority field summary"
  echo "  dist=name  # use name for the package distribution"
  echo "  arch=name  # use name for the package architecture"
  echo "  bin=name   # binary package names"
  echo "  src=name   # source package name"
  echo "  maint=name # use name for the maintainer field"
  echo "  sf=file    # add special file to the list (no checks)"
  echo "  pgp=name   # use name as the pgp userid"
  echo "  sect=name  # section to use for source files and as default"
  echo "Deboverrides:"
  echo "  s=name # set section for further .deb files"
  echo "  p=name # set priority for further .deb files"
  echo "Type \"man $PROG\" for more info"
}

function do_source
{
  if [ -z "$TAR_FILE" ] # no source file
  then
    if [ -n "$DIFF_FILE" ]
    then
      warning "WARNING: source diff file supplied without a source file"
    else
      return $SUCCESS
    fi
  fi
  if [ -z "$SOURCE" -o -z "$VERSION" ]
  then
    stderr "ERROR: <source pkgname> and/or <version> are unknown so"
    stderr "       cannot check source package filenames"
    error  "       use src=<source> and ver=<version>"
  fi
  TAR_FN=`basename "${TAR_FILE}"`
  if [ -n "$TAR_FN" ]
  then
    (echo "$ARCH" | fgrep source > /dev/null) || ARCH="$ARCH source"    
    if [ "$TAR_FN" != "${SOURCE}-${VERSION}.tar.gz" ]
    then
      stderr "ERROR: Source file incorrectly named: $TAR_FN"
      error  "       expected ${SOURCE}-${VERSION}.tar.gz"
    else
      echo "Source file ok: $TAR_FN"
    fi
  fi
  DIFF_FN=`basename "${DIFF_FILE}"`
  if [ -n "$DIFF_FN" ]
  then
    if [ "$DIFF_FN" != "${SOURCE}-${VERSION}.diff.gz" ]
    then
      stderr "ERROR: Source diff file incorrectly named: $DIFF_FN"
      error  "       expected ${SOURCE}-${VERSION}.diff.gz"
    else
      echo "Source diff file ok: $DIFF_FN"
    fi
  fi
}

function do_unknowns
{
  for FILE in $UNK_FILES
  do
    FN=`basename "${FILE}"`
    if (echo $FN | grep -v "^${SOURCE}-${VERSION}" > /dev/null)
    then
      stderr "ERROR: Filename dosnt have correct prefix: $FN"
      error  "       expected ${SOURCE}-${VERSION}<something>"
    else
      echo "File ok: $FN"
    fi
  done
}

function do_files
{
  for FILE in $*
  do
    case $FILE in
      *.deb)
	     if [ ! -f "$FILE" -a ! -L "$FILE" ]
	     then
	       error "ERROR:" "skipping $FILE -- not a file or symlink"
	     fi
	     DEB_CNT=$[ $DEBCNT + 1 ]
	     DEB_FILES="$DEB_FILES $FILE"
	     ;;
      s=*)
	   DEB_FILES="$DEBFILES $FILE"
	   ;;
      p=*)
	   DEB_FILES="$DEBFILES $FILE"
	   ;;
      *.tar.gz)
		if [ ! -f "$FILE" -a ! -L "$FILE" ]
		then
		  error "ERROR:" "skipping $FILE -- not a file or symlink"
		fi
		if [ -z "$TAR_FILE" ]
		then
		  TAR_FILE="$FILE"
		else
		  stderr "ERROR: more than one .tar.gz file,"
		  error  "       got $TAR_FILE and $FILE"
		  UNK_FILES="$UNK_FILES $FILE"
		fi
		;;
      *.diff.gz)
		 if [ ! -f "$FILE" -a ! -L "$FILE" ]
		 then
		   error "ERROR:" "skipping $FILE -- not a file or symlink"
		 fi
		if [ -z "$DIFF_FILE" ]
		then
		  DIFF_FILE="$FILE"
		else
		  stderr "ERROR: more than one .diff.gz file,"
		  error  "       got $DIFF_FILE and $FILE"
		  UNK_FILES="$UNK_FILES $FILE"
		fi
		;;
      *)
	 if [ ! -f "$FILE" -a ! -L "$FILE" ]
	 then
	   error "ERROR:" "skipping $FILE -- not a file or symlink"
	 fi
	 UNK_FILES="$UNK_FILES $FILE"
	 ;;
    esac
  done
  do_debs
  do_source
  do_unknowns
}

function stderr
{
    echo "$*" 1>&2
    return $SUCCESS
}

function warning
{
  stderr "$*"
  WARNINGS=$[ $WARNINGS + 1 ]
}

function error
{
  stderr "$*"
  if [ "$WOPTION" = TRUE ]
  then
    WARNINGS=$[ $WARNINGS + 1 ]
  else
    exit $FAILURE
  fi
}

function do_syntax
{
  for FILE in $*
  do
    if [ ! -r $FILE -a ! -L $FILE ]
    then
      stderr "ERROR:" "file $FILE missing or not readable"
      continue
    fi
    gawk 'BEGIN {
             required1["Date:"]
             required1["Format:"]
             required1["Source:"]
             required1["Maintainer:"]
             required1["Description:"]
             required1["Priority:"]
             required1["Date:"]
             required2["Changes:"]
             err = malformed = misplaced = blanklines = 0
             infield = 0
        } {
             if (substr($0,1,1) == "#") next
             if ( length( $0 ) == 0 || NF == 0) { blanklines++; next } 
             if ( substr($0,1,1) == " " )
             {
                 if ( infield == 0) misplaced++
             } else {
                 if ( substr( $1, length( $1 ) )  != ":" ) { malformed++; next }
                 infield = 1
                 hits[$1]++
                 if ( $1 == "Files:" ) { files_seen = 1 }
             }
        } END {
            #
            # all provided fields have a one-entry limit
            #
            for (field in hits)
            {
                name = substr(field, 1, length(field) - 1)
                if ( hits[field] != 1 )
                {
                    err++
                    print hits[field]" "name" field lines" > "/dev/stderr"
                }
            }
            #
            # check for required fields
            #
            for (field in required1)
            {
                if ( hits[field] == "" )
                {
                    err++
                    name = substr(field, 1, length(field) - 1)
                    print name" field not present" > "/dev/stderr"
                }
            }
            if (files_seen == 1)
            {
                for (field in required2)
                {
                    if ( hits[field] == "" )
                    {
                        err++
                        name = substr(field, 1, length(field) - 1)
                        print "Files seen but "name" field not present" > "/dev/stderr"
                    }
                }
            }
            if (blanklines != 0)
            {
                err++
                print blanklines" illegal blank lines" > "/dev/stderr"
            }
            if (misplaced != 0)
            {
                err++
                print misplaced" misplaced field extension lines" > "/dev/stderr"
            }
            if (malformed != 0)
            {
                err++
                print malformed" field summary lines missing \":\" after field name" > "/dev/stderr"
            }
            exit err
        }' $FILE
        ERRORS=$?
        if [ $ERRORS != 0 ]
        then
            error "ERROR:" "$ERRORS syntax problems in $FILE"
        fi
    done
}

main "$@"

