#!/usr/bin/perl -w

################################################################
#
# Copyright (c) 1995-2014 SUSE Linux Products GmbH
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################

use strict;

# buffer size for reading
my $bufsize = 4*1024*1024;

my ($opt_skip, $opt_disk, $opt_input, $opt_verbose);
$opt_verbose = 0;

while (@ARGV)  {
  if ($ARGV[0] eq '--skip') {
    shift @ARGV;
    $opt_skip = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--disk') {
    shift @ARGV;
    $opt_disk = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--input') {
    shift @ARGV;
    $opt_input = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--verbose' || $ARGV[0] eq '-v') {
    shift @ARGV;
    $opt_verbose++;
    next;
  }
  last;
}

die "need to specify disk image\n" unless $opt_disk;

open(F, '<', $opt_disk) || die "$opt_disk: $!\n";

if ($opt_input) {
  open(S, '<', $opt_input) || die "$opt_input: $!\n";
} else {
  open(S, '<&STDIN') || die "can't dup stdin: $!\n";
}

# skip build status
if ($opt_skip) {
  seek(S, $opt_skip, 0) || die "seek: $!\n";
}

my %done;
while (<S>) {
  chomp;
  last unless length $_;
  my ($filetype, $file, $filesize, $blksize, @blocks) = split(/ /);
  die("invalid input '$_'\n") unless defined($file);
  $file =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
  die("bad file '$file'\n") if "/$file/" =~ /\/\.{0,2}\//s;
  if ($file =~ /^(.*)\//s) {
    die("file without directory: $file\n") unless $done{$1} && $done{$1} eq 'd';
  }
  if ($filetype eq 'd') {	# dir
    print "$file\n" if $opt_verbose && $opt_verbose > 1;
    mkdir($file) || die("mkdir $file: $!\n");
    $done{$file} = 'd';
    next;
  }
  if ($filetype eq 'l') {	# symlink
    my $target = $filesize;
    die("symlink without target\n") unless defined $target;
    $target =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
    die("bad symlink: $target\n") if "/$target/" =~ /\/\.?\//s;
    if ("/$target/" =~ /^(\/\.\.)+\/(.*?)$/s) {
      my ($head, $tail) = ($1, $2);
      die("bad upref in symlink: $target\n") if "/$tail/" =~ /\/\.\.\//s;
      die("bad upref in symlink: $target\n") if ($head =~ y!/!!) > ($file =~ y!/!!);
    } else {
      die("bad upref in symlink: $target\n") if "/$target/" =~ /\/\.\.\//s;
    }
    print "$file\n" if $opt_verbose && !($opt_verbose == 1 && $file =~ /^KIWI\/.*\//);
    symlink($target, $file) || die("symlink $target $file: $!\n");
    $done{$file} = 'l';
    next;
  }
  die("illegal file type: $filetype\n") unless $filetype eq 'f';
  die "invalid input '$_'\n" if !@blocks && $filesize;
  $done{$file} = 'f';
  $filesize = int($filesize);
  if ($filesize == 0) {
    print "$file\n" if $opt_verbose && !($opt_verbose == 1 && $file =~ /^KIWI\/.*\//);
    open (O, '>', $file) or die "$file: $!\n";
    close O;
    next;
  }
  $blksize = int($blksize);
  die "$file: invalid block size $blksize\n" unless $blksize > 0 && $blksize <= $bufsize;
  my $maxblocks = int($bufsize/$blksize);
  print "$file\n" if $opt_verbose && !($opt_verbose == 1 && $file =~ /^KIWI\/.*\//);
  open (O, '>', $file) or die "$file: $!\n";
  for my $block (@blocks) {
    my $end;
    ($block, $end) = split(/-/, $block);
    $block = int($block);
    if ($block == 0) { # a hole!
      $end = (($end || 0) + 1) * $blksize;
      $end = $filesize if $end > $filesize;
      seek(O, $end, 1);
      $filesize -= $end;
      next;
    }
    $end = $block unless $end;
    $end = int($end);
    seek(F, $block*$blksize, 0) || die "$file: seek: $!\n";
    while ($block <= $end && $filesize) {
      my $size;
      if ($end == $block) {
	$size = $blksize;
	++$block;
      } elsif ($maxblocks >= $end-$block) {
	$size = ($end-$block)*$blksize;
	$block += $end-$block;
      } else {
	$size = $maxblocks*$blksize;
	$block += $maxblocks;
      }
      $size = $filesize if $size > $filesize;
      my $buf;
      (sysread(F, $buf, $size) || 0) == $size || die("$file: read: $!\n");
      $filesize -= $size;
      (syswrite(O, $buf) || 0) == length($buf) || die("$file: write error\n");
    }
  }
  close(O) || die("$file: close error: $!\n");
  # sanity check
  die "$file: invalid file size ($filesize byes left)\n" if $filesize != 0;
}
