#!/usr/bin/env ruby
# -*- ruby -*-
#
# Copyright (c) 2001-2004 Akinori MUSHA
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

RCS_ID = %q$Idaemons: /home/cvs/sunshar/sunshar.rb,v 1.13 2004/02/28 14:15:47 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require 'optparse'
require 'fileutils'
require 'shellwords'
require 'stringio'

$USAGE = 'usage'

$strip_level = 0
$force = false
$dryrun = false
$quiet = false
$dir = nil

def info(*s)
  puts(*s) unless $quiet
end

def usage
  print <<-EOF
#{MYNAME} rev.#{RCS_REVISION}

usage:  #{MYNAME} [-fnq] [-p level] [file]
        #{MYNAME} -h
  -d dir  chdir   -- chdir to dir before extracting files
  -f      force   -- allow overwriting, ignore errors
  -h      help    -- show this help
  -n      dry run -- show what would have been extracted
  -p N    strip   -- strip N levels from pathnames (cf. patch(1)\'s -p)
  -q      quiet   -- be quiet
  EOF
end

def main
  params = ARGV.getopts("fhnq", "d:", "p:")

  if params['h']
    usage
    exit 0
  end

  if params['f']
    $force = true
  end

  if params['n']
    $dryrun = true
  end

  if params['q']
    $quiet = true
  end

  $dir = params['d'] || '.'

  if not params['p'].nil?
    $strip_level = params['p'].to_i rescue -1

    if $strip_level < 0
      STDERR.puts "negative value ignored: #{params['p']}"
    end
  end

  nerrors = 0

  if ARGV.empty?
    info "extracting files from stdin into #{$dir}"

    begin
      Dir.chdir($dir) {
	unshar_stream(STDIN)
      }

      info "done."
    rescue => e
      STDERR.puts "error in extracting stdin: #{e.message}"
      nerrors += 1
    end
  else
    for file in ARGV
      info "extracting files from #{file} into #{$dir}"

      begin
	File.open(file) do |f|
	  Dir.chdir($dir) {
	    unshar_stream(f)
	  }
	end

	info "done."
      rescue => e
	STDERR.puts "error in extracting #{file}: #{e.message}"
	nerrors += 1
      end
    end
  end

  exit nerrors
end

def unshar_stream(io)
  e = nil

  while line = io.gets
    if /^(\s*)\# This is a shell archive/ =~ line
      indent = $1.length
      break
    end
  end

  if io.eof?
    raise "not a shell archive."
  end

  f = nil
  prefix = nil
  file = nil
  boundary = nil

  while line = io.gets
    line.slice!(0, indent)

    if f
      if line.strip == boundary
	f.close
	f = nil
	next
      end

      if line.sub!(/^#{Regexp.quote(prefix)}/, '')
	f.print line
      else
	raise "line #{io.lineno}: broken archive: #{line}"
      end

      next
    end

    case line
    when /^exit\s*$/
      break
    when /^echo\s+(.+)$/
      # info $1
    when /^mkdir\s+(?:-p\s+)?(.+)$/
      dir = nil

      Shellwords.shellwords($1).each do |word|
	if /^[^\-]/ =~ word
	  dir = word
	  break
	end
      end

      next if dir.nil?

      dir = strip_filename(dir.strip + '/')
      if dir.chomp('/').empty?
	next
      end

      begin
	FileUtils.mkdir_p(dir) unless $dryrun
	info "c - #{dir}"
      rescue => e
	info "c - #{dir} ... failed: #{e.message}"
	raise e
      end
    when /sed\s+(.+)>(.+)<<(.+)/
      prefix = Shellwords.shellwords($1).first
      file = Shellwords.shellwords($2).first
      boundary = Shellwords.shellwords($3).first

      next unless prefix && file && boundary

      if /s(.)\^(.*)\1\1/ =~ prefix
	prefix = $2
      else
	next
      end

      file = strip_filename(file)

      next if file.empty? || boundary.empty?

      overwrite = false

      if File.exist?(file)
	if $force
	  overwrite = true
	else
	  info "x - #{file} ... skipped"
	  next
	end
      end

      dir = File.dirname(file)

      if !File.directory?(dir + "/.")
	begin
	  FileUtils.mkdir_p(dir) unless $dryrun
	  info "d - #{dir}"
	rescue => e
	  info "d - #{dir} ... failed: #{e.message}"
	  raise e
	end
      end

      begin
        f = $dryrun ? StringIO.new : File.open(file, 'w')
	if overwrite
	  info "x - #{file} ... overwritten"
	else
	  info "x - #{file}"
	end
      rescue => e
	info "x - #{file} ... failed! (#{e.message})"

	if $force
	  f = nil
	  next
	else
	  raise e
	end
      end
    end
  end

  raise e if e
end

def strip_filename(file)
  sfile = file.gsub(%r"/{2,}", "/")

  if 0 < $strip_level
    sfile.sub!(%r"^([^/]*/){1,#{$strip_level}}", '')
  end

  case sfile
  when %r"^[~/]"
    raise "reference to absolute directory: #{file} (use -p N)"
  when %r"(^|/)\.\.(?:/|$)"
    raise "reference to parent directory: #{file} (use -p N)"
  end

  sfile
end

def signal_handler(sig)
  info "\nInterrupted."

  exit 255
end

if $0 == __FILE__
  for sig in [2, 3, 15]
    trap(sig) do
      signal_handler(sig)
    end
  end

  main
end
