#! /bin/sh
# mozwhack --- patch mozilla client executables for netscape.cfg lookup

# $Id: mozwhack,v 1.3 1998/07/20 21:53:27 friedman Exp $

# The contents of this file are subject to the Netscape Public License
# Version 1.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.mozilla.org/NPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is Mozilla Communicator client code, released March
# 31, 1998.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation.  Portions created by Netscape are Copyright (C) 1998
# Netscape Communications Corporation.  All Rights Reserved.
#
# Contributor(s): Noah Friedman <friedman@splode.com>.

# Commentary:

# This program modifies the hardwired path in Mozilla executables used to
# search for `netscape.cfg', the distributed admin stub file ("Mission
# Control", as it was called for Communicator 4.0).
#
# The directory specified must have a subdirectory called `app-defaults' in
# which the netscape.cfg file actually resides.  This is another policy
# that is hardwired into the executable, and this program can't change that.
#
# There is a flag that can be set in the executable (via the --require-cfg
# option) which will cause the executable to abort (without any warning or
# explanation, unfortunately) if the netscape.cfg file cannot be found.  I
# don't recommend enabling this flag; it's too fascist.

# You must have the libAutoAdmin.so plugin in order to use Mission Control.
# That file must be located in $MOZILLA_HOME, not $MOZILLA_HOME/plugins.
# Furthermore, in Communicator 4.03, the MOZILLA_HOME environment variable
# must end with a trailing `/', or the executable will attempt to load the
# wrong file name for the autoadmin module.  (This is fixed in 4.05.)

# The signature and order of the ekit_patch structure, as well as other
# magic data, were obtained from mozilla/cmd/xfe/e_kit_patch.h.  The rest
# of this code was written from scratch by Noah Friedman.

# Code:

# Name by which this script was invoked.
progname=`echo "$0" | sed -e 's/[^\/]*\///g'`

# To prevent hairy quoting and escaping later.
bq='`'
eq="'"

suffix=whacked

usage="Usage: $progname [filename] [cfg dir]

Options are:
-D, --debug                  Turn on shell debugging ($bq${bq}set -x$eq$eq).
-h, --help                   You're looking at it.
-o, --output-file   FILE     Write output to file FILE.  By default, the
                             output file is written to the current working
                             directory with the name of the input file
                             appended by $bq${bq}.$suffix$eq$eq.
-R, --no-require-cfg         Allow mozilla executable to run without
                             netscape.cfg file present.
-r, --require-cfg            Do not allow mozilla executable to run without
                             netscape.cfg file present.
-v, --verbose                Be verbose.
"

# Initialize variables.
# Don't use `unset' since old bourne shells don't have this command.
# Instead, assign them an empty value.
debug=
verbose=
outfile=
perl="${PERL-perl}"
MOZWHACK_VERBOSE=
MOZWHACK_REQUIRE=asis

# Usage: eval "$getopt"; value=$optarg
# or     optarg_optional=t; eval "$getopt"; value=$optarg
#
# This function automatically shifts the positional args as appropriate.
# The argument to an option is optional if the variable `optarg_optional'
# is non-empty.  Otherwise, the argument is required and getopt will cause
# the program to exit on an error.  optarg_optional is reset to be empty
# after every call to getopt.  The argument (if any) is stored in the
# variable `optarg'.
#
# Long option syntax is `--foo=bar' or `--foo bar'.
# For optional args, you must use the `--foo=bar' long option syntax
# if the argument starts with `-', otherwise the argument will be ignored
# and treated as the next option.
#
# Note: because of broken bourne shells, using --foo=bar syntax can
# actually screw the quoting of args that end with trailing newlines.
# Specifically, most shells strip trailing newlines from substituted
# output, regardless of quoting.
getopt='
  {
    optarg=
    case "$1" in
      --*=* ) optarg=`echo "$1" | sed -e "1s/^[^=]*=//"` ; shift ;;
      -* )
        case "${2+set}:$optarg_optional" in
          set: ) optarg="$2" ; shift ; shift ;;
          set:?* )
            case "$2" in
              -* ) shift ;;
              * )  optarg="$2"; shift; shift ;;
            esac
           ;;
          : )
            option="$1"
            case "$option" in
              --*=* ) option=`echo "$option" | sed -e "1s/=.*//;q"` ;;
            esac
            echo "$progname: option $bq$option$eq requires argument." 1>&2
            echo "$progname: use $bq--help$eq to list option syntax." 1>&2
            exit 1
           ;;
          * ) shift ;;
        esac
       ;;
    esac
    optarg_optional=
  }'

# Parse command line arguments.
# Make sure that all wildcarded options are long enough to be unambiguous.
# It's a good idea to document the full long option name in each case.
# Long options which take arguments will need a `*' appended to the
# canonical name to match the value appended after the `=' character.
while : ; do
  case $# in 0) break ;; esac
  case "$1" in
    -D | --debug | --d* )
      set -x
      debug=-d
      shift
     ;;
    -h | --help | --h* )
      echo "$usage" 1>&2
      exit 0
     ;;
    -o | --output-file* | --o* )
      eval "$getopt"
      outfile="$optarg"
     ;;
    -R | --no-require-cfg | --n* )
      MOZWHACK_REQUIRE=no
      shift
     ;;
    -r | --require-cfg | --r* )
      MOZWHACK_REQUIRE=yes
      shift
     ;;
    -v | --verbose | --v* )
      MOZWHACK_VERBOSE=t
      shift
     ;;
    -- )     # Stop option processing
      shift
      break
     ;;
    -? | --* )
      case "$1" in
        --*=* ) arg=`echo "$1" | sed -e 's/=.*//'` ;;
        * )     arg="$1" ;;
      esac
      exec 1>&2
      echo "$progname: unknown or ambiguous option $bq$arg$eq"
      echo "$progname: Use $bq--help$eq for a list of options."
      exit 1
     ;;
    -??* )
      # Split grouped single options into separate args and try again
      optarg="$1"
      shift
      set fnord `echo "x$optarg" | sed -e 's/^x-//;s/\(.\)/-\1 /g'` ${1+"$@"}
      shift
     ;;
    * )
      break
     ;;
  esac
done

MOZWHACK_PROGNAME="$progname"
export MOZWHACK_PROGNAME MOZWHACK_REQUIRE MOZWHACK_VERBOSE

case $# in
  2 ) : ;;
  * )
    echo "$usage" 1>&2
    exit 1
   ;;
esac

infile="$1"
whackdir="$2"
shift
shift

case "$outfile" in
  '' )
    case "$infile" in
      - ) outfile="stdin.$suffix" ;;
      * ) outfile=`echo "$infile" | sed -e 's/\\/*$//;s/.*\\///'`.$suffix ;;
    esac
   ;;
esac

exec $perl $debug - "$infile" "$outfile" "$whackdir" 9<&0 << '__EOF__'

&main (@ARGV);

sub main
{
  local ($infile, $outfile, $whackdir) = @_;
  local ($infile_name, $outfile_name) = ("\`$infile'", "\`$outfile'");

  &initialize ();

  $infile_name  = "standard input"  if ($infile eq "-");
  $outfile_name = "standard output" if ($outfile eq "-");

  $infile  = "&9"      if ($infile eq "-");
  $outfile = "&STDOUT" if ($outfile eq "-");

  &fatal ($infile, "Cannot read and write to same file.")
    if ($infile_name eq $outfile_name);

  open (INFILE,  "<$infile")  || fatal ("open(r)", $infile, "$!");
  open (OUTFILE, ">$outfile") || fatal ("open(w)", $outfile, "$!");

  &verbose ("Reading input from $infile_name.");
  &transform (INFILE, OUTFILE, $whackdir, $outfile);
  close (INFILE);
  close (OUTFILE);
  &fix_perms ($infile, $outfile);
  &verbose ("Output written to $outfile_name.");
}

sub initialize
{
  $EKIT_ENABLED  = 0x01;
  $EKIT_REQUIRED = 0x02;

  $re_ekit = sprintf ("(%c|%c|%c)",
                      $EKIT_ENABLED, $EKIT_REQUIRED,
                      $EKIT_ENABLED | $EKIT_REQUIRED);

  $signature = "\177\222\223\235\112\223\235\112"
             . "\213\230\112\217\127\225\223\236"
             . "\112\237\230\223\233\237\217\112"
             . "\235\236\234\223\230\221\113\0";

  $sig_length     = length ($signature);
  $version_length = 8;

  # This value should be larger than the actual structure size is likely to
  # be; we'll adjust downward.
  $PATH_MAX   = 4096;
  $verified_PATH_MAX = 0;
  &initialize_patch_struct ($PATH_MAX);
}

sub initialize_patch_struct
{
  local ($root_length) = $_[0] + 1;

  $patch_struct_template = "a$sig_length"     # unsigned char signature[32];
                         . "a$version_length" # char version[8];
                         . "a$root_length"    # char root[PATH_MAX+1];
                         . "C";               # unsigned char whacked;

  $patch_struct_len = length (pack ($patch_struct_template, '', '', '', ''));
}

sub transform
{
  local ($in, $out, $whackdir, $outfile_name) = @_;
  local ($buffer, $idx, $idx_offset);
  local ($blocksize) = 1024 * 1024;  # bytes
  local ($bytesread, $total_bytesread, $buflen) = (0, 0, 0);

  # It would have been simpler to just read the entire executable into
  # memory, then do the search/modify on the buffer and write it back out.
  # But Mozilla executables are over 10MB in some cases.
  #
  # This algorithm reads blocks, searches for the signature, and if not
  # found writes out most of the buffer except for the very end
  # in case the signature crosses block boundaries.
  while (1)
    {
      $bytesread = read ($in, $buffer, $blocksize, $buflen);
      last if (! defined ($bytesread) || $bytesread < 1);
      $total_bytesread += $bytesread;
      $buflen += $bytesread;

      $idx_offset = $buflen - $bytesread - $sig_lenth;
      $idx_offset = 0 if ($idx_offset < 0);
      $idx = index ($buffer, $signature, $idx_offset);
      if ($idx >= 0)
        {
          local ($offset) = $total_bytesread - $buflen + $idx;
          &verbose ("Found signature at offset $offset.");

          while ($buflen < ($idx_offset + $patch_struct_len))
            {
              &verbose ("Structure crosses block boundary; reading...");

              $bytesread = read ($in, $buffer, $blocksize, $buflen);
              &fatal ("Unexpected end of file while reading structure")
                if (! defined ($bytesread) || $bytesread < 1);
              $total_bytesread += $bytesread;
              $buflen += $bytesread;
            }

          &determine_patch_struct_size ($idx, \$buffer);
          if (length ($whackdir) > $PATH_MAX)
            {
              close ($in);
              close ($out);
              unlink ($outfile_name);
              &fatal ($whackdir, "name exceeds max length ($PATH_MAX)");
            }
          &whack ($idx, \$buffer, $whackdir);
        }

      if (($buflen - $sig_length) > $sig_length)
        {
          print $out substr ($buffer, 0, $buflen - $sig_length);
          $buffer = substr ($buffer, $buflen - $sig_length);
          $buflen = $sig_length;
        }
    }
  # Flush remaining buffer.
  print $out $buffer;
}

# This function guesses the size of the patch structure by looking for the
# EKIT_* key bytes, none of which should appear between the signature and
# the proper end of the structure.  We can't just hardcode the length
# because it varies from platform to platform (e.g. 255 bytes for SunOS
# 4.x, 1024 bytes for Solaris, etc.)
#
# This function depends on patch structure member order.
sub determine_patch_struct_size
{
  local ($idx, $buffer) = @_;
  local ($x) = substr ($$buffer,
                       $idx + $sig_length + $version_length,
                       $patch_struct_len);

  if ($x =~ m/$re_ekit/go)
    {
      # Substract one for the pos of the `whacked' flag which we just
      # found, and subtract one more for the pathname null terminator.
      $PATH_MAX = pos ($x) - 2;
      $verified_PATH_MAX = 1;
      &verbose ("Max allowed path length = $PATH_MAX bytes");
    }
  else
    {
      $PATH_MAX = 255;
      print STDERR "warning: cannot determine max path length; "
                 . "assumuming $PATH_MAX.\n";
    }
  &initialize_patch_struct ($PATH_MAX);
}

sub whack
{
  local ($idx, $buffer, $whackdir) = @_;
  local ($datastr) = substr ($$buffer, $idx, $patch_struct_len);
  local (%data) = unpack_patch_struct ($datastr);
  local ($require) = &getenv ("MOZWHACK_REQUIRE");

  &verbose ("Old directory", $data{'root'});
  &verbose ("New directory", $whackdir);
  $data{'root'} = $whackdir;

  &verbose ("This binary previously edited by version $data{version}")
    if (substr ($data{'version'}, 0, 1) ne chr (0));
  $data{'version'} = '0.1.mw';

  if ($verified_PATH_MAX)
    {
      if ($require eq 'yes')
        {
          $data{'whacked'} |= $EKIT_REQUIRED;
          &verbose ("Executable will require config file.");
        }
      elsif ($require eq 'no')
        {
          $data{'whacked'} |= $EKIT_REQUIRED;
          $data{'whacked'} ^= $EKIT_REQUIRED;
          &verbose ("Executable will NOT require config file.");
        }
      else
        {
          &verbose ("Executable requires config file",
                    ($data{'whacked'} & $EKIT_REQUIRED)? "yes" : "no");
        }
    }
  else
    {
      if ($require eq 'asis')
        {
          &verbose ("Can't tell whether executable requires config file.");
        }
      else
        {
          &verbose ("Cannot change requirement for config file.");
        }
    }

  $datastr = &pack_patch_struct (%data);
  # Ewww, functions as lvalues.  But this modifies the buffer in place,
  # which we want.
  substr ($$buffer, $idx, $patch_struct_len) = $datastr;
}

sub fix_perms
{
  local ($infile, $outfile) = @_;
  local (@attrs) = stat ($infile);

  return if (! defined (@attrs));
  chmod ($attrs[2], $outfile);
  chown ($attrs[4], $attrs[5], $outfile);
  chmod ($attrs[2], $outfile);
}

sub unpack_patch_struct
{
  local (@vals) = unpack ($patch_struct_template, $_[0]);
  local (%new);
  $new{'signature'} = $vals[0];
  $new{'version'}   = $vals[1];
  $new{'root'}      = $vals[2];
  $new{'whacked'}   = $vals[3];
  return %new;
}

sub pack_patch_struct
{
  local (%d) = @_;
  pack ($patch_struct_template,
        $d{'signature'}, $d{'version'}, $d{'root'}, $d{'whacked'});
}

sub err
{
  print (STDERR join(": ", &getenv ('MOZWHACK_PROGNAME'), @_) . "\n");
}

sub fatal
{
  unshift (@_, 'FATAL');
  &err (@_);
  exit (1);
}

sub verbose
{
  if (&getenv ("MOZWHACK_VERBOSE"))
    {
      local ($s) = sprintf ("%s", join(": ", @_));
      local ($i) = index ($s, chr (0));
      $s = substr ($s, 0, $i) if ($i > 0);
      print STDERR "$s\n";
    }
}

sub getenv
{
  local ($x) = $ENV{$_[0]};
  return $x if (defined ($x) && $x ne '');
  return undef;
}

sub putenv
{
  $ENV{$_[0]} = $_[1];
}

__EOF__

# mozwhack ends here