#
#    W_cache.pm:     manipulate ny's cache
#                                                    Tomo.M(2006/06/18)
package W_cache;

# cache header structure
#    var       len      type
# c_ver       4        ulong      cache version
# filesize    4        ulong      file size
# b_ref       4        ulong      block referenced bytes
# utime       4        long       update time stamp/unix date
# data_md5    16       char[16]   md5 of data
# trip        11       char[11]   trip string
# hoge_data    2       char[2]    unused
# bbsflag      1       uchar
# name_len     1       uchar      length of original file name 
# upflag       1       uchar
# name_sum     2       ushort     data sum of the data
# name       254       char[254]  original file name
# b_flag   1-32768                0/1 map for block presence.

# 0x9d01505e,0x14672a9a,0x5ec6dc74,0x2c840d8f


require Exporter;
@ISA = qw (Exporter);
@EXPORT = qw ( new
	       parse_header
	       parse_block
	       bulk_conv
	       c_ver f_size b_ref mtime file_md5 trip bbsflag
	       n_len upflag f_name dmap
	       );
use Crypt::RC4;
use Digest::MD5;
use Jcode;
use File::Basename;
use strict 'vars';
use vars qw ( $C0_KEY $C1_KEY $C2_KEY $BR_KEY $CK_MASK
	      $HEAD_S $B_SIZE $FN_KEY $MAP_SIZE);

$C0_KEY = "CHeader";
$C1_KEY = "header";
$C2_KEY = "adsfu6";
$BR_KEY = "\x32\x54\x76\x98";
$CK_MASK = 0xff;
$HEAD_S = (16+4+4+4+4+16+11+2+1+1+1+2+254);   # header base size (320)
$B_SIZE = 65536;    # block size
$MAP_SIZE = 32768;  # dmap size

$FN_KEY = "\x9d\x01\x50\x5e"
    . "\x14\x67\x2a\x9a"
    . "\x5e\xc6\xdc\x74"
    . "\x2c\x84\x0d\x8f";

sub new {
    my $this = shift;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    if ($_[0]) {
	if (@_ % 2) {
	    $self->{file_path} = shift;
	}
	$self->set(@_);
    }

    my $fh = undef;
    if ($self->{file_path} eq "-") {
	$fh = *STDIN;
    } elsif ($self->{file_path}) {
	open($fh, "<", $self->{file_path})
	    or return undef;
    }

    if (!defined $self->{fh}) {
	$self->{fh} = $fh;
    }

    my $header = undef;
    if ($self->{Encode}) {
	$self->{f_name} = $self->{file_path}
	    unless defined $self->{f_name};
	$self->{f_name} = basename($self->{f_name});
    } else {
	seek($fh, 0, 0);
	my $nr = read($fh, $header, $HEAD_S+$MAP_SIZE); # at most of header
	#return undef unless $nr >= $HEAD_S;
    }
    $self->{header} = $header;
    return($self);
}

sub set {
    my $this = shift;
    my %args = @_;

    foreach (keys %args) {
	$this->{$_} = $args{$_};
    }

    $this;
}

sub get {
    my $this = shift;
    my $key = shift;

    return($this->{$key});
}


sub decode_header {
    my $this = shift;
    my ($mctx, $buf, $ctx, $f_size, $b_num,
	$hmd5, $ofs, $header, $dheader);

    $header = shift || $this->{header};
    return undef unless length($header) >= $HEAD_S;

    $mctx = Digest::MD5->new();
    return undef unless defined $mctx;

    # md5 hash of header
    $hmd5 = substr($header, 0, 16);

    $ctx = Crypt::RC4->new($C1_KEY);
    return undef unless defined $ctx;

    # version
    $buf = substr($header, 16, 4);
    $buf = $ctx->RC4($buf);
    $mctx->add($buf);
    $this->{c_ver} = unpack("L", $buf);

    $ctx = Crypt::RC4->new($C2_KEY);

    $buf = substr($header, 16+4, $HEAD_S-(16+4));
    $dheader = $ctx->RC4($buf);
    $mctx->add($dheader);
    $ofs = 0;

    # file size
    $buf = substr($dheader, $ofs, 4); $ofs += 4;

    $f_size = unpack("L", $buf); 
    $this->{f_size} = $f_size;

    $b_num = int(($f_size+$B_SIZE-1) / $B_SIZE);
    $this->{b_num} = $b_num;
    $this->{head_size} = $HEAD_S+$b_num;

    # block ref count
    $buf = substr($dheader, $ofs, 4); $ofs += 4;

    $buf = $buf ^ $BR_KEY;
    my $b_ref = unpack("L", $buf);
    $this->{b_ref} = $b_ref;

    # mtime
    $buf = substr($dheader, $ofs, 4); $ofs += 4;
    $this->{mtime} = unpack("L", $buf);

    # md5 hash of file
    $buf = substr($dheader, $ofs, 16); $ofs += 16;
    $this->{file_md5} = $buf;

    # trip string
    $buf = substr($dheader, $ofs, 11); $ofs += 11;
    $this->{trip} = $buf;

    # dummy
    $buf = substr($dheader, $ofs, 2); $ofs += 2;

    # bbsflag
    $buf = substr($dheader, $ofs, 1); $ofs += 1;
    $this->{bbsflag} = $buf;

    # name_len
    $buf = substr($dheader, $ofs, 1); $ofs += 1;
    my $n_len = unpack("C", $buf);
    $this->{n_len} = $n_len;

    # upflag
    $buf = substr($dheader, $ofs, 1); $ofs += 1;
    $this->{upflag} = $buf;

    # name_sum
    $buf = substr($dheader, $ofs, 2); $ofs += 2;
    my $sum = unpack("S", $buf);

    # f_name
    $buf = substr($dheader, $ofs, 254); $ofs += 254;
    my $k = $sum % 256;
    my $ctx2 = Crypt::RC4->new(pack("C", $k));
    return undef unless defined $ctx2;
    $buf = $ctx2->RC4($buf);
    $buf =~ /^(.{$n_len})/s;
    $buf = $1;
    $this->{f_name} = $buf;

    # block map
    $buf = substr($header, $HEAD_S, $b_num);
    $buf = $ctx->RC4($buf);
    $mctx->add($buf);
    $this->{dmap} = $buf;

    my $dd = $mctx->digest;
    if ($dd ne $hmd5) {
	print STDERR "header may be corrupted\n";
	printf STDERR "chk md5: %s\n", unpack("H32", $hmd5);
	printf STDERR "dat md5: %s\n", unpack("H32", $dd);
    }

    return $this;
}

sub parse_header { shift->decode_header(@_); }

sub encode_header {
    my $this = shift;
    my ($mctx, $d, $ctx, $b_num, $size,	$mtime, $header, $nheader);

    return undef unless defined $this->{fh};

    # file md5
    $mctx = Digest::MD5->new();
    return undef unless defined $mctx;
    $mctx->addfile($this->{fh});
    $this->{file_md5} = $mctx->digest unless defined $this->{file_md5};
    #print STDERR unpack("H32", $this->{file_md5}^$FN_KEY) . "\n";

    # file size check
    $size = (stat($this->{fh}))[7];
    $this->{f_size} = $size unless defined $this->{f_size};
    return unless $this->{f_size};

    $header = '';
    $mctx = $mctx->new();
    return undef unless defined $mctx;

    $ctx = Crypt::RC4->new($C1_KEY);
    return undef unless defined $ctx;

    # version
    $this->{c_ver} = 0x47470105 unless defined $this->{c_ver};
    $d = pack("L", $this->{c_ver});
    $mctx->add($d);
    $header = $ctx->RC4($d);

    # file size
    $d = pack("L", $this->{f_size});
    $nheader .= $d;

    $b_num = int(($this->{f_size}+$B_SIZE-1) / $B_SIZE);
    $this->{head_size} = $HEAD_S+$b_num;
    $this->{b_num} = $b_num;

    # block reference count
    my $b_ref = (defined $this->{b_ref}
		 ? $this->{b_ref}
		 : int($b_num * $B_SIZE / 99));
    $d = pack("L", $b_ref);
    $d ^= $BR_KEY;
    $nheader .= $d;

    # mtime
    $mtime = (stat($this->{fh}))[9] || time;
    $mtime = (defined $this->{mtime} ? $this->{mtime} : $mtime);
    $d = pack("L", $mtime);
    $nheader .= $d;

    # file md5
    $d = pack("a16", $this->{file_md5});
    $nheader .= $d;

    # trip
    $d = pack("Z11", $this->{trip});
    $nheader .= $d;

    # dummy
    $d = pack("C2", 0, 0);
    $nheader .= $d;

    # bbs flag
    $d = pack("C", (defined $this->{bbsflag} ? $this->{bbsflag} : 0));
    $nheader .= $d;

    # file name length
    $d = pack("C", length($this->{f_name}));
    $nheader .= $d;

    # up flag
    $d = pack("C", (defined $this->{upflag} ? $this->{upflag} : 0));
    $nheader .= $d;

    # file name check sum
    my @n = split(//, $this->{f_name});
    my $sum = 0;
    foreach (@n) {
	$sum += ord;
    }
    $d = pack("S", $sum & 0xffff);
    $nheader .= $d;

    # file name
    my $k = $sum % 256;
    my $ctx2 = Crypt::RC4->new(pack("C", $k));
    return undef unless defined $ctx2;
    $d = $ctx2->RC4(pack("a254", $this->{f_name}));
    $nheader .= $d;

    $mctx->add($nheader);
    $ctx = Crypt::RC4->new($C2_KEY);
    return undef unless defined $ctx;

    $header .= $ctx->RC4($nheader);

    # block map
    $d = pack("C[$b_num]", (0x1) x $b_num);
    $mctx->add($d);
    $header .= $ctx->RC4($d);

    $this->{header} =  $mctx->digest . $header;
    return $this;
}

sub decode_block {
    my $this = shift;
    my ($seq, $buf) = @_;
    my ($fh, $move_bytes, $lbuf, $nr);

    $fh = $this->{fh};
    return undef unless defined $fh;
    return undef if ($seq < 1 || $seq > $this->{b_num});

    $move_bytes = $this->{head_size} + (($B_SIZE+16)*($seq-1));
    seek($fh, $move_bytes, 0);

    if (($nr = $this->read_block($fh, $seq, \$lbuf))) {
	if (ref($buf) eq "SCALAR") {
	    $$buf = $lbuf;
	    return $nr;
	} else {
	    return $lbuf;
	}
    }
    return $nr;
}

sub parse_block { shift->decode_block(@_); }

sub bulk_decode {
    my $this = shift;
    my ($ofh) = @_;
    my ($fh, $nr, $seq, $lbuf);

    $fh = $this->{fh};
    return undef unless defined $fh;

    seek($fh, $this->{head_size}, 0);

    for ($seq=1; $seq <= $this->{b_num}; $seq++) {
	$nr = $this->read_block($fh, $seq, \$lbuf);
	return undef unless $nr;
	print $ofh $lbuf;
    }
    close($fh);
    return 1;
}

sub bulk_conv { shift->bulk_decode(@_); }

sub read_block {
    my $this = shift;
    my ($fh, $seq, $buf) = @_;
    my ($ctx, $nr, $lbuf, $mctx, $md5, $key);

    # read md5 hash + block
    $nr = read($fh, $lbuf, 16+$B_SIZE);
    return undef if $nr <= 0;

    $md5 = substr($lbuf, 0, 16);
    $lbuf = substr($lbuf, 16);
    
    if ($seq > 1) {
	$key = sprintf("%d %s",
		       $seq-1,
		       unpack("H32", $this->file_md5));
    } else {
	$key = $C1_KEY;
    }

    #print STDERR "block: $seq\n";
    #print STDERR "key: $key\n;

    $ctx = Crypt::RC4->new($key);
    $lbuf = $ctx->RC4($lbuf);

    unless ($this->{NoCheck}) {
	$mctx = Digest::MD5->new();
	$mctx->add($lbuf);
	my $dd = $mctx->digest;

	if ($dd ne $md5) {
	    print STDERR "block $seq may be corrupted\n";
	    printf STDERR "chk md5: %s\n", unpack("H32", $md5);
	    printf STDERR "dat md5: %s\n", unpack("H32", $dd);
	}
    }

    if (ref($buf) eq "SCALAR") {
	$$buf = $lbuf;
	return $nr;
    } else {
	return $lbuf;
    }

}

sub bulk_encode {
    my $this = shift;
    my ($ofh) = @_;
    my ($fh, $nr, $nw, $seq, $lbuf);

    $fh = $this->{fh};
    return undef unless defined $fh;

    if (!defined $this->{header}) {
	$this->encode_header();
    }

    if (!defined $ofh) {
	my $ofname = '%' . unpack("H32", $this->{file_md5}^$FN_KEY);
	open ($ofh, ">$ofname") or return undef;
    }

    if (defined $this->{header} && defined $ofh) {
	print $ofh $this->{header};
	seek($fh, 0, 0);
	for ($seq=1; $seq <= $this->{b_num}; $seq++) {
	    $nr = read($fh, $lbuf, $B_SIZE);
	    $this->write_block($ofh, $seq, \$lbuf);
	}
    }
    close($fh);
    return 1;
}

sub write_block {
    my $this = shift;
    my ($ofh, $seq, $buf) = @_;
    my ($ctx, $nw, $lbuf, $mctx, $md5, $key);

    $mctx = Digest::MD5->new();
    return undef unless defined $mctx;
    $mctx->add($$buf);
    $md5 = $mctx->digest;

    if ($seq > 1) {
	$key = sprintf("%d %s",
		       $seq-1,
		       unpack("H32", $this->file_md5));
    } else {
	$key = $C1_KEY;
    }

    #print STDERR "block: $seq\n";
    #print STDERR "key: $key\n;

    $ctx = Crypt::RC4->new($key);
    return undef unless defined $ctx;
    $lbuf = $ctx->RC4($$buf);
    print $ofh $md5 . $lbuf;
}


sub c_ver    { shift->{c_ver}; }
sub f_size   { shift->{f_size}; }
sub b_num    { shift->{b_num}; }
sub b_ref    { shift->{b_ref}; }
sub mtime    { shift->{mtime}; }
sub file_md5 { shift->{file_md5}; }
sub trip     { shift->{trip}; }
sub bbsflag  { shift->{bbsflag}; }
sub n_len    { shift->{n_len}; }
sub upflag   { shift->{upflag}; }
sub f_name   { shift->{f_name}; }
sub dmap     { shift->{dmap}; }
sub head_size  { shift->{head_size}; }
sub header   { shift->{header}; }

sub hexdump {
    my ($str) = @_;

    my @dat = unpack('C*', $str);
    my $len = $#dat;

    my ($i, $j) = (0, 0);
    while ($j <= $len) {
        print "=";
        for ($i=0; $i<16; $i++) {
            last if ($j+$i > $len);
            printf " %02x", $dat[$j+$i];
        }
        print "   " x (16-$i);
        print "  ";
        for ($i=0; $i<16; $i++) {
            last if ($j+$i > $len);
            my $c = $dat[$j+$i];
            printf "%c", (($c<0x20||$c>0x7f) ? ord(".") : $c);
        }
        $j+=$i;
        print "\n";
    }
}

1;
__END__

=head1 NAME

W_cache - winny's cache manipulation.

=head1 SYNOPSIS

    use W_cache;

    $cache = W_cache->new( [ file_path ] );
    $cache->parse_header();
    $file_name = $cache->f_name;
    $file_size = $cache->f_size;
    $block_count = $cache->b_num;

    $buf;
    $bytes = $cache->parse_block($block_num, \$buf);
     or
    $buf = $cache->parse_block($block_num);
    $block_num is 1 .. $cache->b_num

=head1 DESCRIPTION

This module manipulates winny's cache file.


=head1 METHODS

=over 4

=item new ( [ file_path ] )

Returns ny's cache object related to file of 'file_path'.

=item parse_header ( [ $header_data ] ) 

Parse ny's cache file header, and get various file attributes.

=item parse_block ( block_seq [, buffer_ref ] )

Decrypt cache file's block No. block_seq. Result will be in buffer.
or will be return value.

=item bulk_conv ( $outfh )

Decrypt completed cache file's block. Result will be written to
 file handle.

=item c_ver

Returns cache version.

=item f_size

Returns size in bytes of original file.

=item b_num

Returns total block number.

=item b_ref

Returns block reference count.

=item mtime

Returns last modified time in unix time format.

=item file_md5

Returns md5 hash of original file.

=item trip

Returns 'trip' string applied by originator.

=item bbsflag

Returns bbsflag.

=item n_len

Returns length(byte) of file name.

=item upflag

Returns upflag.

=item f_name

Returns original file name (Shift_JIS when Japanese).

=item dmap

Returns Map data of block presence.

block population will be derived by,

    my @dmap = unpack "C*", $cache->dmap;
    my @a = grep(/1/, @dmap);
    if ($b_num) {
         print "b_num: $b_num\n";
         printf "%.1f %\n", (($#a+1)/$b_num)*100;
    }

block presence map will be,

    for (my $i=0; $i<=$#dmap; $i++) {
        printf "%d ", $dmap[$i];
        print "\n" if $i % 32 == 31;
    }
    print "\n";

=back


=head1 SEE ALSO

=head1 AUTHOR

Tomo.M <tomo at c-wind.com>

=head1 COPYRIGHT

Copyright (c) 2006 Tomo.M. All rights reserved.

=cut
