#!/usr/bin/env python
#
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed 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.

"""
Generates a self extracting archive with a license click through.

Usage:
  generate-self-extracting-archive.py $OUTPUT_FILE $INPUT_ARCHIVE $COMMENT $LICENSE_FILE

  The comment will be included at the beginning of the output archive file.

Output:
  The output of the script is a single executable file that when run will
  display the provided license and if the user accepts extract the wrapped
  archive.

  The layout of the output file is roughly:
   * Executable shell script that extracts the archive
   * Actual archive contents
   * Zip file containing the license
"""

import tempfile
import sys
import os
import zipfile

_HEADER_TEMPLATE = """#!/bin/bash
#
{comment_line}
#
# Usage is subject to the enclosed license agreement

echo
echo The license for this software will now be displayed.
echo You must agree to this license before using this software.
echo
echo -n Press Enter to view the license
read dummy
echo
more << EndOfLicense
{license}
EndOfLicense

if test $? != 0
then
  echo "ERROR: Couldn't display license file" 1>&2
  exit 1
fi
echo
echo -n 'Type "I ACCEPT" if you agree to the terms of the license: '
read typed
if test "$typed" != "I ACCEPT"
then
  echo
  echo "You didn't accept the license. Extraction aborted."
  exit 2
fi
echo
{extract_command}
if test $? != 0
then
  echo
  echo "ERROR: Couldn't extract files." 1>&2
  exit 3
else
  echo
  echo "Files extracted successfully."
fi
exit 0
"""

_PIPE_CHUNK_SIZE = 1048576
def _pipe_bytes(src, dst):
  while True:
    b = src.read(_PIPE_CHUNK_SIZE)
    if not b:
      break
    dst.write(b)

_MAX_OFFSET_WIDTH = 20
def _generate_extract_command(start, size, extract_name):
  """Generate the extract command.

  The length of this string must be constant no matter what the start and end
  offsets are so that its length can be computed before the actual command is
  generated.

  Args:
    start: offset in bytes of the start of the wrapped file
    size: size in bytes of the wrapped file
    extract_name: of the file to create when extracted

  """
  # start gets an extra character for the '+'
  # for tail +1 is the start of the file, not +0
  start_str = ('+%d' % (start + 1)).rjust(_MAX_OFFSET_WIDTH + 1)
  if len(start_str) != _MAX_OFFSET_WIDTH + 1:
    raise Exception('Start offset too large (%d)' % start)

  size_str = ('%d' % size).rjust(_MAX_OFFSET_WIDTH)
  if len(size_str) != _MAX_OFFSET_WIDTH:
    raise Exception('Size too large (%d)' % size)

  return "tail -c %s $0 | head -c %s > %s\n" % (start_str, size_str, extract_name)


def main(argv):
  if len(argv) != 5:
    print 'generate-self-extracting-archive.py expects exactly 4 arguments'
    sys.exit(1)

  output_filename = argv[1]
  input_archive_filename = argv[2]
  comment = argv[3]
  license_filename = argv[4]

  input_archive_size = os.stat(input_archive_filename).st_size

  with open(license_filename, 'r') as license_file:
    license = license_file.read()

  if not license:
    print 'License file was empty'
    sys.exit(1)

  if 'SOFTWARE LICENSE AGREEMENT' not in license:
    print 'License does not look like a license'
    sys.exit(1)

  comment_line = '# %s\n' % comment
  extract_name = os.path.basename(input_archive_filename)

  # Compute the size of the header before writing the file out. This is required
  # so that the extract command, which uses the contents offset, can be created
  # and included inside the header.
  header_for_size = _HEADER_TEMPLATE.format(
      comment_line=comment_line,
      license=license,
      extract_command=_generate_extract_command(0, 0, extract_name),
  )
  header_size = len(header_for_size.encode('utf-8'))

  # write the final output
  with open(output_filename, 'wb') as output:
    output.write(_HEADER_TEMPLATE.format(
        comment_line=comment_line,
        license=license,
        extract_command=_generate_extract_command(header_size, input_archive_size, extract_name),
    ).encode('utf-8'))

    with open(input_archive_filename, 'rb') as input_file:
      _pipe_bytes(input_file, output)

    with tempfile.TemporaryFile() as trailing_zip:
      with zipfile.ZipFile(trailing_zip, 'w') as myzip:
        myzip.writestr('license.txt', license, compress_type=zipfile.ZIP_STORED)

      # append the trailing zip to the end of the file
      trailing_zip.seek(0)
      _pipe_bytes(trailing_zip, output)

  umask = os.umask(0)
  os.umask(umask)
  os.chmod(output_filename, 0o777 & ~umask)

if __name__ == "__main__":
  main(sys.argv)
