#! /usr/bin/perl
#
#   squashfs dumper
#
#   Copyright (C) 2004 Enrik Berkhan <enrik.berkhan@inka.de>
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   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; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

use strict;
use warnings;

use Getopt::Std;
$Getopt::Std::STANDARD_HELP_VERSION = 1;

$main::VERSION = "1.0";

sub main::HELP_MESSAGE {
  usage();
  exit 1;
}

our %opt;
getopts('C:htxvf:', \%opt);

sub usage {
  print STDERR <<EOF
usage:	dumpsquashfs -h
	dumpsquashfs -t [-v] -f <input>
	dumpsquashfs -x [-v] -C <dir> -f <input>
	-h: show this help
	-t: list files containd in <input>
	-x: extract files contained in <input>
	-v: verbose operation
	-C <dir>: chdir(<dir>) before extracting
	-f <input>: squashfs image to read from, '-' is stdin
EOF
}

if ($opt{h}) {
  usage();
  if (scalar keys %opt == 1) {
    exit 0;
  } else {
    exit 1;
  }
}

unless ($opt{x} xor $opt{t}) {
  usage();
  exit 1;
}

if ($opt{f} eq '') {
  usage();
  exit(1);
} elsif ($opt{f} eq '-') {
  open FS, "<&STDIN";
} else {
  open FS, "<$opt{f}" or die "can't read $opt{f}: $!";
}

if ($opt{x} && $opt{C}) {
  chdir($opt{C}) or die "can't chdir($opt{C})";
}

my $r = new SquashFS::Reader(\*FS);
$opt{_reader} = $r;
$r->directory_walk($r->{root_directory}, $r->{root_inode}, '.', \&walk_cb_early, \&walk_cb_late, \%opt);

sub pretty_mode {
  my $type = shift;
  my $perms = shift;
  my $bits;

  if ($type == 1) {
    $bits = 'd';
  } elsif ($type == 2) {
    $bits = '-';
  } elsif ($type == 3) {
    $bits = 'l';
  } elsif ($type == 4) {
    $bits = 'b';
  } elsif ($type == 5) {
    $bits = 'c';
  } elsif ($type == 6) {
    $bits = 'p';
  } elsif ($type == 7) {
    $bits = 's';
  } else {
    $bits = '?';
  }

  $bits .= $perms & 0x100? "r" : "-";
  $bits .= $perms & 0x080? "w" : "-";
  $bits .= $perms & 0x040? "x" : "-";
  $bits .= $perms & 0x020? "r" : "-";
  $bits .= $perms & 0x010? "w" : "-";
  $bits .= $perms & 0x008? "x" : "-";
  $bits .= $perms & 0x004? "r" : "-";
  $bits .= $perms & 0x002? "w" : "-";
  $bits .= $perms & 0x001? "x" : "-";

  return $bits;
}

sub walk_cb_early {
  my $inode = shift;
  my $pinode = shift;
  my $name = shift;
  my $opt = shift;

  my ($mtime, $size, $major, $minor);
  my $type = $inode->{type};
  if ($type > 2) {
    $mtime = $pinode->{mtime};
  } else {
    $mtime = $inode->{mtime};
  }

  if ($type == 4 || $type == 5) {
    $major = $inode->{rdev}>>8;
    $minor = $inode->{rdev}&0xff;
    $size = sprintf("  %3d, %3d", $major, $minor);
  } else {
    $size = sprintf("%10d", $inode->{file_size}?$inode->{file_size}:0);
  }

  if ($opt->{t} && !$opt->{v} || ($opt->{x} && $opt->{v})) {
    print "$name\n";
  } elsif ($opt{t} && $opt->{v}) {
    printf "%s %5d %5d %s %s %s%s\n", pretty_mode($type, $inode->{mode}),
      $inode->{uid}, $inode->{guid}, $size, scalar localtime($mtime), $name, $type==3?" -> $inode->{symlink}":"";
  }

  if ($opt->{x}) {
    if ($type == 1) {
      mkdir("$name");
    } elsif ($type == 2) {
      open FILE, ">$name" or die "can't write $name";
      $opt->{_reader}->dump_file($inode, \*FILE);
      close FILE;
    } elsif ($type == 3) {
      symlink($inode->{symlink}, "$name");
    } elsif ($type == 4) {
      system("mknod $name b $major $minor") and warn("could not create block device $name ($major, $minor)");
    } elsif ($type == 5) {
      system("mknod $name c $major $minor") and warn("could not create character device $name ($major, $minor)");
    } elsif ($type == 6) {
      system("mkfifo $name") and warn("could not create name pipe $name");
    } elsif ($type == 7) {
      use Socket;
      my $sun = sockaddr_un($name);
      socket SOCK, PF_UNIX, SOCK_STREAM, 0 and bind(SOCK, $sun) or warn("could not create socket $name");
    } else {
      warn("type $type not yet supported in extraction");
    }
  }

  # add: uid, gid

  return;
}

sub walk_cb_late {
  my $inode = shift;
  my $pinode = shift;
  my $name = shift;
  my $opt = shift;

  my ($mtime);
  my $type = $inode->{type};
  if ($type > 2) {
    $mtime = $pinode->{mtime};
  } else {
    $mtime = $inode->{mtime};
  }

  if ($opt->{x}) {
    if ($type != 3) {
      chmod $inode->{mode}, "./$name";
      utime $mtime, $mtime, "./$name";
    }
  }
}


package SquashFS::Reader;

use Compress::Zlib;

BEGIN {
  $SquashFS::Reader::debug = 0;
}

sub new {
  my $class = shift;
  my $input = shift;
  my $self = {};
  $self->{in} = $input;

  my $buf;
  my $l = read $input, $buf, 63; 
  if ($l < 63) {
    warn("short read while reading SquashFS superblock");
    return undef;
  }
  my ($s_magic, $inodes, $bytes_used,
      $uid_start, $guid_start, $inode_table_start, $directory_table_start,
      $s_major, $s_minor, $block_size_1, $block_log, $flags,
      $no_uids, $no_guids, $mkfs_time, $root_inode_offset, $root_inode_blk,
      $root_inode_hi, $block_size, $fragments, $fragment_table_start) =
    unpack("VVVVVVVvvvvCCCVvvVVVV", $buf);
  if ($s_magic == 0x68737173) {
    ($s_magic, $inodes, $bytes_used,
     $uid_start, $guid_start, $inode_table_start, $directory_table_start,
     $s_major, $s_minor, $block_size_1, $block_log, $flags,
     $no_uids, $no_guids, $mkfs_time, $root_inode_hi, $root_inode_blk,
     $root_inode_offset, $block_size, $fragments, $fragment_table_start) =
    unpack("NNNNNNNnnnnCCCNNnnNNN", $buf);
    $self->{be} = 1;
  } elsif ($s_magic == 0x73717368) {
    $self->{be} = 0;
  } else {
    die("wrong superblock magic $s_magic");
  }

  if ($s_major != 2) {
    die("wrong SquashFS major version $s_major");
  }
  $self->{block_log} = $block_log;
  $self->{block_size} = $block_size;
  $self->{check} = 1 if $flags & 4;

  bless ($self, $class);

  if ($uid_start) {
    my $uids;
    seek $input, $uid_start, 0 or die "seek: $!";
    my $l = read $input, $uids, $no_uids * 4; die "short read" if $l != $no_uids * 4;
    if ($self->{be}) {
      $self->{uids} = [ unpack("N$no_uids", $uids) ];
    } else {
      $self->{uids} = [ unpack("V$no_uids", $uids) ];
    }
  } else {
    $self->{uids} = [0];
  }

  if ($guid_start) {
    my $guids;
    seek $input, $guid_start, 0 or die "seek: $!";
    my $l = read $input, $guids, $no_guids * 4; die "short read" if $l != $no_guids * 4;
    if ($self->{be}) {
      $self->{gids} = [ unpack("N$no_guids", $guids) ];
    } else {
      $self->{gids} = [ unpack("V$no_guids", $guids) ];
    }
  } else {
    $self->{gids} = [0];
  }

  $self->read_inode_table($inode_table_start, $directory_table_start);
  $self->read_directory_table($directory_table_start, $fragment_table_start);
  my $fragment_table_end;
  { use integer; $fragment_table_end = $fragment_table_start + 4*($fragments*8 + 8192 - 1)/8192 };
  $self->read_fragment_index($fragment_table_start, $fragment_table_end);

  $self->{root_inode} = $self->read_inode($root_inode_blk, $root_inode_offset);
  $self->{root_directory} = $self->read_directory($self->{root_inode});
  
  return $self;
}

sub directory_walk {
  my $self = shift;
  my $dir = shift;
  my $pinode = shift;
  my $pname = shift;
  my $callback1 = shift;
  my $callback2 = shift;
  my $cbparam = shift;

  foreach my $name (sort keys %$dir) {
    my $inode = $dir->{$name};
    if ($callback1) {
      &$callback1($inode, $pinode, $pname.'/'.$name, $cbparam);
    }
    if ($inode->{type} == 1) {
      my $subdir = $self->read_directory($inode);
      $self->directory_walk($subdir, $inode, $pname.'/'.$name, $callback1, $callback2, $cbparam);
    }
    if ($callback2) {
      &$callback2($inode, $pinode, $pname.'/'.$name, $cbparam);
    }
  }
}

sub read_block {
  my ($self) = shift;
  my ($offset) = shift;
  my ($len, $buf);

  print STDERR "read_block: seek $$offset\n" if $SquashFS::Reader::debug;
  seek $self->{in}, $$offset, 0 or die "seek: $!";
  my $l = read $self->{in}, $len, 2; die "short read: $!" if $l != 2;
  if ($self->{be}) {
    $len = unpack("n", $len);
  } else {
    $len = unpack("v", $len);
  }
  my $is_compressed = !($len & 0x8000);
  $len = $len & 0x7fff;
  $$offset += 2 + $len;
  if ($self->{check}) {
    $l = read $self->{in}, $buf, 1; die "short read: $!" if $l != 1;
    $$offset++;
  }
  $l = read $self->{in}, $buf, $len; die "short read: $!" if $l != $len;
  if ($is_compressed) {
    my $res = uncompress($buf);
    die "umcompress failed" unless defined $res;
    return \$res;
  } else {
    return \$buf;
  }
}

sub read_inode_table {
  my ($self) = shift;
  my ($start) = shift;
  my ($end) = shift;

  my $inode_table_start = $start;
  $self->{inode_table} = '';
  while ($start < $end) {
    $self->{inode_offsets}->{$start - $inode_table_start} = length($self->{inode_table});
    $self->{inode_table} .= ${ $self->read_block(\$start) };
    print STDERR "read_inode_table ", $start - $inode_table_start, "\n" if $SquashFS::Reader::debug;
  }
}

sub read_directory_table {
  my ($self) = shift;
  my ($start) = shift;
  my ($end) = shift;

  my $directory_table_start = $start;
  $self->{directory_table} = '';
  while ($start < $end) {
    $self->{directory_offsets}->{$start - $directory_table_start} = length($self->{directory_table});
    $self->{directory_table} .= ${ $self->read_block(\$start) };
    print STDERR "read_directory_table ", $start - $directory_table_start, "\n" if $SquashFS::Reader::debug;
  }
}

sub read_fragment_index {
  my ($self) = shift;
  my ($start) = shift;
  my ($end) = shift;

  while ($start < $end) {
    my $offset;

    print STDERR "read_fragment_index: $start $end\n" if $SquashFS::Reader::debug;
    seek $self->{in}, $start, 0 or die "seek: $!";
    my $l = read $self->{in}, $offset, 4; die "short read" if $l != 4;
    if ($self->{be}) {
      $offset = unpack("N", $offset);
    } else {
      $offset = unpack("V", $offset);
    }
    print STDERR "read_fragment_table: $offset\n" if $SquashFS::Reader::debug;
    $start += 4;
    $self->{fragment_table} .= ${ $self->read_block(\$offset) };
  }
}

sub dump_file {
  my ($self) = shift;
  my ($inode) = shift;
  my ($out) = shift;

  die "not a plain file" unless $inode->{type} == 2;
  my ($offset) = $inode->{start_block};
  my ($sizes) = $inode->{block_list};
  my ($fragment) = $inode->{fragment};
  my ($frag_offset) = $inode->{offset};
  my ($filesize) = $inode->{file_size};

  seek $self->{in}, $offset, 0 or die "seek: $!";
  foreach my $len (@$sizes) {
    my ($buf);
    my $is_compressed = !($len & $self->{block_size});
    my $len = $len & ~$self->{block_size};
    printf STDERR "read_block_list: %scompressed len %d\n", $is_compressed?"":"un", $len if $SquashFS::Reader::debug;
    my $l = read $self->{in}, $buf, $len; die "short read: $!" if $l != $len;
    if ($is_compressed) {
      my $res = uncompress($buf);
      die "uncompress failed" unless defined $res;
      print $out $res;
      $filesize -= length($res);
    } else {
      print $out $buf;
      $filesize -= length($buf);
    }
  }
  printf STDERR "filesize $filesize fragment $fragment\n" if $SquashFS::Reader::debug;
  if ($filesize && $fragment > -1) {
    my ($buf);
    my ($fstart, $fsize);
    if ($self->{be}) {
      ($fstart, $fsize) = unpack("NN", substr($self->{fragment_table}, $fragment*8));
    } else {
      ($fstart, $fsize) = unpack("VV", substr($self->{fragment_table}, $fragment*8));
    }
    my $is_compressed = !($fsize & $self->{block_size});
    $fsize = $fsize & ~$self->{block_size};
    seek $self->{in}, $fstart, 0 or die "seek: $!";
    my $l = read $self->{in}, $buf, $fsize; die "short read: $!" if $l != $fsize;
    if ($is_compressed) {
      my $res = uncompress($buf);
      die "uncompress failed" unless defined $res;
      print STDERR "fragment $fragment len ", length($res), " offset $offset\n" if $SquashFS::Reader::debug;
      print $out substr($res, $frag_offset, $filesize);
      $filesize = 0;
    } else {
      print $out substr($buf, $frag_offset, $filesize);
      $filesize = 0;
    }
  }
  if ($filesize) {
    die "short file?";
  }
}

sub read_inode {
  my $self = shift;
  my $c_offset = shift;
  my $offset = shift;

  print STDERR "reading inode $c_offset\n" if $SquashFS::Reader::debug;
  if (!defined $self->{inode_offsets}->{$c_offset}) {
    die "inode $c_offset not in cache?";
  }
  my $inode = substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 4);
  print STDERR unpack("H*", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} +  $offset, 32)), "\n"
    if $SquashFS::Reader::debug > 1;
  my ($type, $mode, $uid, $guid);
  if ($self->{be}) {
    ($mode, $uid, $guid) = unpack("nCC", $inode);
    $type = $mode >> 12;
    $mode = $mode & 0xfff
  } else {
    ($mode, $uid, $guid) = unpack("vCC", $inode);
    $type = $mode & 0xf;
    $mode = $mode >> 4;
  }
  $uid = $self->{uids}->[$uid];
  if ($guid == 255) {
    $guid = $uid;
  } else {
    $guid = $self->{gids}->[$guid];
  }
  printf STDERR "type %d mode %o uid %d guid %d\n", $type, $mode, $uid, $guid
    if $SquashFS::Reader::debug;
  if ($type == 1) { # DIR
    return $self->read_dir_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 2) { # FILE
    return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 3) { # SYMLINK
    return $self->read_symlink_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 4) { # BLK
    return $self->read_dev_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 5) { # CHR
    return $self->read_dev_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 6) { # FIFO
    return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } elsif ($type == 7) { # SOCKET
    return $self->read_reg_inode($c_offset, $offset+4, $type, $mode, $uid, $guid);
  } else {
    die "unknown inode type $type";
  }
}

sub read_dev_inode {
  my $self = shift;
  my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_;
  my $rdev;

  if ($self->{be}) {
    $rdev = unpack("n", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} +  $offset, 2));
  } else {
    $rdev = unpack("v", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} +  $offset, 2));
  }
  return {type => $type, mode => $mode, uid => $uid,
          guid => $guid, rdev => $rdev};
}

sub read_symlink_inode {
  my $self = shift;
  my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_;
  my $symlink;

  if ($self->{be}) {
    $symlink = unpack("n/a", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset));
  } else {
    $symlink = unpack("v/a", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset));
  }
  return {type => $type, mode => $mode, uid => $uid,
          guid => $guid, symlink => $symlink};
}

sub read_reg_inode {
  my $self = shift;
  my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_;
  my ($mtime, $start_block, $fragment, $off, $file_size, @block_list, $blocks);

  if ($self->{be}) {
    ($mtime, $start_block, $fragment, $off, $file_size) = 
      unpack("NNNNN", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 20));
  } else {
    ($mtime, $start_block, $fragment, $off, $file_size) = 
      unpack("VVVVV", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 20));
  }
  $fragment = unpack("l", pack("L", $fragment));
  { use integer; $blocks = ($file_size + $self->{block_size} -1 ) / $self->{block_size}; }
  if ($fragment > -1) { $blocks-- }
  if ($type == 2) {
    print STDERR "reg_inode blocks $blocks\n" if $SquashFS::Reader::debug;
    if ($self->{be}) {
      @block_list = unpack("N$blocks", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset + 20));
    } else {
      @block_list = unpack("V$blocks", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset + 20));
    }
  }
  return {type => $type, mode => $mode, uid => $uid,
          guid => $guid, mtime => $mtime, start_block => $start_block,
	  fragment => $fragment, offset => $off, file_size => $file_size, block_list => \@block_list };
}

sub read_dir_inode {
  my $self = shift;
  my ($c_offset, $offset, $type, $mode, $uid, $guid) = @_;
  my ($file_size, $off, $mtime, $start_block, $t1, $t2, $t3);

  if ($self->{be}) {
    ($off, $mtime, $t1, $t2, $t3) =
      unpack("NNCCC", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 11));
    $file_size = $off >> 13;
    $off = $off & 0x1fff
  } else {
    ($off, $mtime, $t1, $t2, $t3) =
      unpack("VVCCC", substr($self->{inode_table}, $self->{inode_offsets}->{$c_offset} + $offset, 11));
    $file_size = $off & 0x7ffff;
    $off = $off >> 19;
  }
  $start_block = ($t3<<16) | ($t2<<8) | $t1;
  
  return {type => $type, mode => $mode, uid => $uid, guid => $guid,
          file_size => $file_size, offset => $off, mtime => $mtime,
	  start_block => $start_block};
}

sub read_directory {
  my $self = shift;
  my $inode = shift;

  my $c_offset = $inode->{start_block};
  my $offset = $inode->{offset};
  my $size = $inode->{file_size};

  print STDERR "reading dir $c_offset\n" if $SquashFS::Reader::debug;
  if (!defined $self->{directory_offsets}->{$c_offset}) {
    die "directory $c_offset not in cache?";
  }
  my $dir = substr($self->{directory_table}, $self->{directory_offsets}->{$c_offset} + $offset, $size);
  print STDERR unpack("H*", substr($self->{directory_table}, $self->{directory_offsets}->{$c_offset} + $offset, 32)), "\n"
    if $SquashFS::Reader::debug > 1;

  my %dir;
  while (length($dir)) {
    my ($count, $t1, $t2, $t3) = unpack("CCCC", $dir);
    $dir = substr($dir, 4);
    $count++;
    my $start_block = $t1 | ($t2 << 8) | ($t3 << 16);
    while($count) {
      my ($type, $offset, $len, $name);
      if ($self->{be}) {
        ($offset, $len) = unpack("nC", $dir); $dir = substr($dir, 3);
        $type = $offset & 0x7;
        $offset = $offset >> 3;
      } else {
        ($offset, $len) = unpack("vC", $dir); $dir = substr($dir, 3);
        $type = $offset >> 13;
        $offset = $offset & 0x1fff;
      }
      $len++;
      $name = unpack("a$len", $dir); $dir = substr($dir, $len);
      $count--;
      $dir{$name} = $self->read_inode($start_block, $offset);
      if ($type != $dir{$name}->{type}) {
        die "read_directory: dir-type != inode-type";
      }
    }
  }
  return \%dir;
}

