#!perl -w use strict; use warnings; $|=1; # (C) 2003-2007 Willem Jan Hengeveld # Web: http://www.xs4all.nl/~itsme/ # http://wiki.xda-developers.com/ # # $Id: $ # # problem: files with RELOC section, the reloc section needs more padding. # # 6fe efe : fileentry ( 6fe for modules, efe for files ) # 6fd cfd : sectionentry # efb : nameentry # fff : deleted entry # # todo: # add reloc info: ~/projects/wince/mamaich/imgfs060412/recmod/RecMod.cpp # bug: # an incorrect fffb entry is added before the imgfs section # it should stay 'ffffffff ffffffff', instead '000014ff fffbffff' is added. # package SimpleFile; use strict; our @ISA= qw(IO::File); #sub new { my $class= shift; return $class->SUPER::new(@_); } sub filesize { return -s $_[0]; } package Reader; use strict; use warnings; use IO::File; sub new { my ($type, $fh, $base)= @_; my $self= bless { fh=>$fh, baseofs=>$base, }, $type; return $self; } sub ReadDword { my ($self, $ofs)= @_; $self->{fh}->seek($self->cv2file($ofs), SEEK_SET) or return; my $data; $self->{fh}->read($data, 4) or return; return unpack("V", $data); } sub WriteDword { my ($self, $ofs, $value)= @_; $self->{fh}->seek($self->cv2file($ofs), SEEK_SET) or return; $self->{fh}->write(pack("V", $value)) or return; #printf("W[%08lx] : 0x%x\n", $ofs, $value); } package TyphReader; use strict; use warnings; use IO::File; our @ISA=qw(Reader); sub ReadData { my ($self, $ofs, $len)= @_; $self->{fh}->seek($self->cv2file($ofs), SEEK_SET) or return; my $data=""; $self->{fh}->read($data, $len) or return; return $data; } sub WriteData { my ($self, $ofs, $data)= @_; $self->{fh}->seek($self->cv2file($ofs), SEEK_SET) or return; $self->{fh}->write($data) or return; #printf("W[%08lx] : %s\n", $ofs, unpack("H*", $data)); } sub cv2file { my ($self, $ofs)= @_; return $self->{baseofs} + $ofs; } sub filesize { my ($self)= @_; return $self->{fh}->filesize()-$self->{baseofs}; } package HimaReader; use strict; use warnings; use IO::File; our @ISA=qw(Reader); sub ReadData { my ($self, $ofs, $len)= @_; my $data=""; while ($len) { my $want= $len; my $realofs= $self->cv2file($ofs); $self->{fh}->seek($realofs, SEEK_SET) or return; if ($want>0x40000-($realofs&0x3ffff)) { $want= 0x40000-($realofs&0x3ffff); } $self->{fh}->read($data, $want, length($data)) or return; $len -= $want; $ofs += $want; } return $data; } sub WriteData { my ($self, $ofs, $data)= @_; my $dofs= 0; while ($dofs < length($data)) { my $want= length($data)-$dofs; my $realofs= $self->cv2file($ofs); $self->{fh}->seek($realofs, SEEK_SET) or return; if ($want>0x40000-($realofs&0x3ffff)) { $want= 0x40000-($realofs&0x3ffff); } $self->{fh}->write(substr($data, $dofs, $want)) or return; #printf("W[%08lx] : %s\n", $ofs, unpack("H*", substr($data, $dofs, $want))); $ofs += $want; $dofs += $want; } return $data; } sub cv2file { my ($self, $ofs)= @_; return $self->{baseofs} + int($ofs/0x3f000)*0x40000+($ofs%0x3f000); } sub filesize { my ($self)= @_; return ($self->{fh}->filesize()/0x40000)*0x3f000-$self->{baseofs}; } package cvtime; use strict; use warnings; use POSIX; sub convert2unixtime { my $wtime= shift; my @w= unpack("VV", $wtime); return int(($w[1]*(2**32)+$w[0])/10000000.0-11644473600) } sub convert2mstime { my $utime= shift; my $wtime= ($utime+11644473600)*10000000; return pack("VV", $wtime%(2**32), $wtime/(2**32)); } sub unix2string { return POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime $_[0]); } package cvstring; use strict; use warnings; sub unicode2string { return pack("U*", unpack("v*", $_[0])); } sub string2unicode { return pack("v*", unpack("U*", $_[0])); } ################################################################################################### ################################################################################################### # common interface to Entries: # XXXEntry->new($reader) # $ent->save($ofs) # XXXEntry->load($reader, $ofs) package DirEntry; use strict; use warnings; sub calc_name_hash { my $hash= lc pack("C*", map { $_&0xff } unpack("C*", $_[0])); $hash =~ s/(.{4})\..{3}$/$1/; return substr($hash,0,2).substr($hash,-2,2); } # decodes a namedata struct ( offset 0xC in a dir entry ) sub parse_namedata { my %name; ( $name{length}, # 00 v WORD filename length : <=4 -> char[4]+ptr = literal filename. $name{flags}, # 02 v WORD flag: 2= filename ptr is direntry, 0= filenameptr is datablock $name{hash}, # 04 a4 char[4] $name{dataptr}, # 08 V ptr to filename file ( see flag for type ) )= unpack("vva4V", $_[0]); if ($name{length}<=4) { $name{name}= cvstring::unicode2string(substr($_[0], 4, 2*$name{length})); $name{dataptr}= 0; $name{hash}= ""; } return \%name; } # get name from a decoded namedata struct sub get_name { my ($self)= @_; if (defined $self->{name}{name}) { return $self->{name}{name}; } if ($self->{name}{flags} & 2) { my $nameentry= NameEntry->load($self->{reader}, $self->{name}{dataptr}, $self->{name}{length}); $self->{name}{name}= $nameentry->{name}; } else { $self->{name}{name}= cvstring::unicode2string($self->{reader}->ReadData($self->{name}{dataptr}, $self->{name}{length}*2)); } $self->{name}{name} =~ s/\x00+$//g; return $self->{name}{name}; } sub setname { my ($self, $newname, $alloc)= @_; $self->{name}{length}= length($newname); $self->{name}{name}= $newname; $self->{name}{hash}= calc_name_hash($newname); if (length($newname)<=4) { $self->{name}{dataptr}= 0; $self->{name}{flags}= 0; } elsif (length($newname)<=24) { $self->{name}{dataptr}= $alloc->allocent("n"); $self->{name}{flags}= 2; my $nent= NameEntry->new($self->{reader}); $nent->{name}= $self->{name}{name}; $nent->save($self->{name}{dataptr}); } else { my $uname= cvstring::string2unicode($newname); my $roundedsize= $alloc->roundsize(length($uname)); $self->{name}{dataptr}= $alloc->allocchunk($roundedsize, "n"); $self->{name}{flags}= 0; $self->{reader}->WriteData($self->{name}{dataptr}, $uname.("\x00" x ($roundedsize-length($uname)))); } $self->save($self->{ofs}); } sub getnamedata { my ($self)= @_; my $uname= pack 'v*', unpack 'U*', $self->{name}{name}; if (length($uname)<=8) { return pack("vva8", length($self->{name}{name}), 0, $uname); } else { return pack("vva4V", length($self->{name}{name}), $self->{name}{flags}, $self->{name}{hash}, $self->{name}{dataptr}); } } sub datatable_iterator { my ($self, $chunkfn)= @_; return if $self->{indexblock}==0 || $self->{indexsize}==0; #printf("datatable[%08lx] iterator\n", $self->{ofs}); my $block= $self->{reader}->ReadData($self->{indexblock}, $self->{indexsize}); for (my $iofs= 0 ; $iofs($dataofs, $compsize, $fullsize); } } sub nameiterator { my ($self, $entryfn, $chunkfn)= @_; #printf("name[%08lx] iterator\n", $self->{ofs}); if ($self->{name}{length}<=4) { # } elsif ($self->{name}{flags}&2) { $entryfn->($self->{name}{dataptr}); } else { $chunkfn->($self->{name}{dataptr}, $self->{name}{length}*2); } } package SectionEntry; use strict; use warnings; our @ISA=qw(DirEntry); sub load { my ($class, $reader, $ofs)= @_; my $self= bless { reader=>$reader }, $class; $self->{ofs}= $ofs; $self->{data}= $reader->ReadData($ofs, 4*13); $self->{magic}= unpack("V", $self->{data}); die sprintf("%08lx not a section entry: magic=%08lx\n", $ofs, $self->{magic}) unless $self->{magic}==0xfffff6fd; my ($namedata, $alwaysnul); ( $self->{magic}, # 00 V $self->{nextdatatable}, # 04 V always 0 : nextdatatable offset $self->{nextsection}, # 08 V dirptr to next section entry $namedata, # 0c a12 FS_NAME struct $self->{size}, # 18 V total section size $self->{indexblock}, # 1c V dataptr,size ptr to index block of section data $self->{indexsize}, # 20 V $alwaysnul, # 24 a16 ) = unpack("VVVa12VVVa16", $self->{data}); $self->{name}= DirEntry::parse_namedata($namedata); warn sprintf("%08lx: unexpected data: ndt=%x\n", $ofs, $self->{nextdatatable}) if $self->{nextdatatable}; warn sprintf("%08lx: unexpected data: +24=%s\n", $ofs, unpack("H*", $alwaysnul)) unless $alwaysnul =~ /^\x00+$/; return $self; } sub new { my ($class, $reader)= @_; my $self= bless { reader=>$reader, ofs=>0, magic=>0xfffff6fd, nextdatatable=>0, nextsection=>0, size=>0, indexblock=>0, indexsize=>0, name=>{ name=>"" }, }, $class; return $self; } sub save { my ($self, $ofs)= @_; my $namedata= $self->getnamedata(); $self->{ofs}= $ofs; $self->{data}= pack("VVVa12VVVa16", $self->{magic}, # 00 V $self->{nextdatatable}, # 04 V always 0 : nextdatatable offset $self->{nextsection}, # 08 V dirptr to next section entry $namedata, # 0c a12 FS_NAME struct $self->{size}, # 18 V total section size $self->{indexblock}, # 1c V dataptr,size ptr to index block of section data $self->{indexsize}, # 20 V "\x00" x 16); $self->{reader}->WriteData($ofs, $self->{data}); } sub dump { my ($self)= @_; printf("%08lx: s->%08lx %9d i->%08lx:%08lx %s\n", $self->{magic}, $self->{nextsection}, $self->{size}, $self->{indexblock}, $self->{indexsize}, $self->{name}{name}); } package FileEntry; use strict; use warnings; our @ISA=qw(DirEntry); sub load { my ($class, $reader, $ofs)= @_; my $self= bless { reader=>$reader, ofs=>$ofs }, $class; $self->{data}= $reader->ReadData($ofs, 4*13); $self->{magic}= unpack("V", $self->{data}); die sprintf("%08lx not a file entry: magic=%08lx\n", $ofs, $self->{magic}) unless $self->{magic}==0xfffff6fe || $self->{magic}==0xfffffefe; my ($namedata, $alwaysnul, $tsdata); ( $self->{magic}, # 00 V $self->{nextdatatable}, # 04 V always 0 : nextdatatable offset $self->{nextsection}, # 08 V dirptr to next section entry $namedata, # 0c a12 FS_NAME struct $self->{size}, # 18 V total file size $self->{attributes}, # 1c V file attributes $tsdata, # 20 a8 timestamp $self->{reserved}, # 28 V always 0 $self->{indexblock}, # 2c V dataptr,length to index block of file data $self->{indexsize}, # 30 V ) = unpack("VVVa12VVa8VVV", $self->{data}); $self->{timestamp}= cvtime::convert2unixtime($tsdata); $self->{name}= DirEntry::parse_namedata($namedata); warn sprintf("%08lx: unexpected data: ndt=%x\n", $ofs, $self->{nextdatatable}) if $self->{nextdatatable}; warn sprintf("%08lx: unexpected data: res=%x\n", $ofs, $self->{reserved}) if $self->{reserved}; return $self; } sub new { my ($class, $reader)= @_; my $self= bless { reader=>$reader, ofs=>0, magic=>0xfffff6fe, nextdatatable=>0, nextsection=>0, size=>0, attributes=>0x41, timestamp=>0, indexblock=>0, reserved=>0, indexsize=>0, name=>{ name=>"" }, }, $class; return $self; } sub save { my ($self, $ofs)=@_; my $tsdata= cvtime::convert2mstime($self->{timestamp}); my $namedata= $self->getnamedata(); $self->{ofs}= $ofs; $self->{data}= pack("VVVa12VVa8VVV", $self->{magic}, # 00 V $self->{nextdatatable}, # 04 V always 0 : nextdatatable offset $self->{nextsection}, # 08 V dirptr to next section entry $namedata, # 0c a12 FS_NAME struct $self->{size}, # 18 V total file size $self->{attributes}, # 1c V file attributes $tsdata, # 20 a8 timestamp $self->{reserved}, # 28 V always 0 $self->{indexblock}, # 2c V dataptr,length to index block of file data $self->{indexsize}, # 30 V ); $self->{reader}->WriteData($ofs, $self->{data}); } sub dump { my ($self)= @_; printf("%08lx: s->%08lx %9d [%08lx] %s i->%08lx:%08lx %s\n", $self->{ofs}, $self->{nextsection}, $self->{size}, $self->{attributes}, cvtime::unix2string($self->{timestamp}), $self->{indexblock}, $self->{indexsize}, $self->{name}{name}); $self->section_iterator( sub { my ($section)= @_; printf("%08lx:%08lx: n->%08lx %9d i->%08lx:%08lx %s\n", $self->{ofs},$section->{ofs}, $section->{nextsection}, $section->{size}, $section->{indexblock}, $section->{indexsize}, $section->{name}{name}); } ); } sub section_iterator { my ($self, $fn)= @_; #printf("section[%08lx] iterator\n", $self->{ofs}); my $section; for (my $sectionofs= $self->{nextsection} ; $sectionofs ; $sectionofs= $section->{nextsection}) { $section= SectionEntry->load($self->{reader}, $sectionofs); $fn->($section); } } sub saveto { my ($self, $saveas, $decomp)= @_; my $fh= IO::File->new($saveas, "w") or die "$saveas: $!\n"; binmode $fh; # todo: warn user that module extraction is not yet supported $self->datatable_iterator( sub { my ($ofs, $comp, $full)= @_; my $block= $self->{reader}->ReadData($ofs, $comp); $fh->print($decomp->($block, $full)); } ); $fh->close(); } package NameEntry; use strict; use warnings; sub load { my ($class, $reader, $ofs, $len)= @_; my $self= bless { reader=>$reader }, $class; $self->{ofs}= $ofs; $self->{data}= $reader->ReadData($ofs, 4*13); $self->{magic}= unpack("V", $self->{data}); die sprintf("%08lx not a name entry: magic=%08lx\n", $ofs, $self->{magic}) unless $self->{magic}==0xfffffefb; $self->{name}= cvstring::unicode2string(substr($self->{data}, 4, $len*2)); return $self; } sub new { my ($class, $reader)= @_; my $self= bless { reader=>$reader, magic=>0xfffffefb, ofs=>0, name=>"" }, $class; return $self; } sub save { my ($self, $ofs)= @_; $self->{ofs}= $ofs; my $uname= pack("Va48", $self->{magic}, cvstring::string2unicode($self->{name})); die "NameEntry : name too large\n" if length($uname)>0x34; $self->{reader}->WriteData($ofs, $uname); } sub dump { my ($self)= @_; printf("%08lx: %s\n", $self->{magic}, $self->{name}); } ############################################################################# ############################################################################# package ImgfsFile; use strict; use warnings; use XdaDevelopers::CompressUtils; # 'h' : first dirchunk # 'd' : dirchunk # 'i' : indexchunk for section # 'a' : datachunk for section # 'n' : namechunk for section # 'I' : indexchunk for file # 'A' : datachunk for file # 'N' : namechunk for file # ' ' : free sub mark { my ($self, $ofs, $size, $tag)= @_; die sprintf("mark(%s):invalid ofs/size: %08lx/%08lx\n", $tag, $ofs, $size) if ($size==0 || $size>=$self->{reader}->filesize() || $ofs>=$self->{reader}->filesize()); $size= $self->roundsize($size); die sprintf("unaligned chunk: %08lx/%08lx\n", $ofs, $size) if $ofs%$self->{imgfs}{chunksize} || $size%$self->{imgfs}{chunksize}; my ($ix, $n)= (int($ofs/$self->{imgfs}{chunksize}), int($size/$self->{imgfs}{chunksize})); if ($tag ne " " && substr($self->{chunkmap}, $ix, $n) ne " " x $n) { warn sprintf("chunk %08lx/%08lx is already: '%s'\n", $ofs, $size, substr($self->{chunkmap}, $ix, $n)); } substr($self->{chunkmap}, $ix, $n)= $tag x $n; } sub allocdirblock { my ($self)= @_; my $blocksize= $self->{imgfs}{chunksperblock}; my $bsm1= $blocksize-1; while ($self->{chunkmap} =~ / (?= {$bsm1})/g) { my $ix= pos($self->{chunkmap})-1; if (($ix % $blocksize)==0) { my $ofs= $ix*$self->{imgfs}{chunksize}; $self->mark($ofs, $blocksize*$self->{imgfs}{chunksize}, "d"); return $ofs; } } print join "", map { sprintf("%08x: %s\n", $_*0x100, substr($self->{chunkmap}, $_*0x100, 0x100)) } 0..int((length($self->{chunkmap})+0xff)/0x100)-1; die sprintf("could not alloc dirblock\n"); } sub allocchunk { my ($self, $size, $tag)= @_; return 0 if $size==0; die sprintf("unaligned size in allocchunk: %x\n", $size) if $size%$self->{imgfs}{chunksize}; my $n= $size/$self->{imgfs}{chunksize}; my $ix= index($self->{chunkmap}, " " x $n); if ($ix==-1) { die sprintf("could not alloc chunk of size 0x%x\n", $size); } my $ofs= $ix*$self->{imgfs}{chunksize}; $self->mark($ofs, $size, $tag); return $ofs; } sub erase { my ($self, $ofs, $size)= @_; printf("erase data: %08lx/%08lx\n", $ofs, $size) if $::verbose>2; $self->mark($ofs, $size, " "); $self->{reader}->WriteData($ofs, "\xff" x $size); } # 'f' : file entry # 'n' : file-name entry # 'N' : section-name entry # ' ' : free entry # 's' : section entry sub markent { my ($self, $ofs, $tag)= @_; die sprintf("markent(%s): invalid ofs: %08lx\n", $tag, $ofs) if ($ofs<0x200 || $ofs>=$self->{reader}->filesize()); my $ix= $self->entrytoindex($ofs); die sprintf("dirent ix too large: %d\n", $ix) if $ix>=length($self->{entrymap}); if ($tag ne " " && substr($self->{entrymap}, $ix, 1) ne " ") { warn sprintf("entry %08lx ( %d ) is already %s\n", $ofs, $ix, $tag); } substr($self->{entrymap}, $ix, 1)= $tag; } sub allocent { my ($self, $tag)= @_; my $ix= index($self->{entrymap}, " "); if ($ix==-1) { my $blkofs= $self->allocdirblock(); $ix= $self->registerdirblock($blkofs); my $prevblocknr= int($ix/$self->{imgfs}{entriesperblock})-1; my $prevofs= $self->{dir_nr2ofs}{$prevblocknr}; $self->{reader}->WriteData($prevofs, pack("VV", 0x2f5314ce, $blkofs)); $self->{reader}->WriteData($blkofs, pack("VV", 0x2f5314ce, 0).("\xff" x ($self->{imgfs}{dirblocksize}-8))); #printf("extended dir: %08lx->%08lx\n", $prevofs, $blkofs); } #printf("l(m)=0x%x, i=%x\n", length($self->{entrymap}), $ix); #printf("o2n: %s\n", join(" ", map { sprintf("%x:%x", $_, $self->{dir_ofs2nr}{$_}) } sort {$a<=>$b} keys %{$self->{dir_ofs2nr}})); #printf("n2o: %s\n", join(" ", map { sprintf("%x:%x", $_, $self->{dir_nr2ofs}{$_}) } sort {$a<=>$b} keys %{$self->{dir_nr2ofs}})); my $ofs= $self->indextoentry($ix); $self->markent($ofs, $tag); #printf("allocent->%08lx\n", $ofs); return $ofs; } sub eraseent { my ($self, $ofs)= @_; $self->markent($ofs, " "); $self->{reader}->WriteData($ofs, "\xff" x $self->{imgfs}{direntsize}); } # returns index of first entry of new block sub registerdirblock { my ($self, $ofs)= @_; my $ix= length($self->{entrymap}); my $blocknr= int($ix/$self->{imgfs}{entriesperblock}); $self->{dir_ofs2nr}{$ofs}= $blocknr; $self->{dir_nr2ofs}{$blocknr}= $ofs; $self->{entrymap} .= " " x $self->{imgfs}{entriesperblock}; #printf("adding %d dir entries: total=%d\n", $self->{imgfs}{entriesperblock}, length($self->{entrymap})); return $ix; } sub entrytoindex { my ($self, $ofs)= @_; my ($dirblockofs, $blockofs)= (int($ofs/$self->{imgfs}{dirblocksize})*$self->{imgfs}{dirblocksize}, $ofs % $self->{imgfs}{dirblocksize}); die sprintf("unaligned direntry offset %08lx\n", $ofs) if ($blockofs-8)%$self->{imgfs}{direntsize}; die sprintf("unknown dirblock ofs 0x%x:%x (%08lx)\n", $dirblockofs, $blockofs, $ofs) if !exists $self->{dir_ofs2nr}{$dirblockofs}; my $ix= $self->{dir_ofs2nr}{$dirblockofs}*$self->{imgfs}{entriesperblock} + int(($blockofs-8)/$self->{imgfs}{direntsize}); return $ix; } sub indextoentry { my ($self, $ix)= @_; my ($blocknr, $blockix)= (int($ix/$self->{imgfs}{entriesperblock}), $ix%$self->{imgfs}{entriesperblock}); die sprintf("unknown dirblock nr %d:%d ( %d )\n", $blocknr, $blockix, $ix) if !exists $self->{dir_nr2ofs}{$blocknr}; return $self->{dir_nr2ofs}{$blocknr}+8+$self->{imgfs}{direntsize}*$blockix; } sub roundsize { my ($self, $size)=@_; return 0 if $size==0; return (($size-1)|($self->{imgfs}{chunksize}-1))+1; } sub new { my ($class, $reader, $imgfs)= @_; my $self= bless { reader=>$reader, imgfs=>$imgfs, chunkmap=>" " x int($reader->filesize()/$imgfs->{chunksize}), entrymap=>"", dir_ofs2nr => {}, dir_nr2ofs => {}, filebyname=> {}, }, $class; $self->mark(0, $self->{imgfs}{firstdirofs}, "h"); # fill alloc maps $self->dirblock_iterator( sub { my $ofs= shift; #printf("imgfs: dirblock(%08lx)\n", $ofs); $self->mark($ofs, $imgfs->{dirblocksize}, "d"); $self->registerdirblock($ofs); } ); $self->direntry_iterator( sub { my $file= shift; #printf("imgfs: file(%08lx)\n", $file->{ofs}); $self->markent($file->{ofs}, "f"); my $name= $file->get_name(); warn sprintf("duplicate name: %s\n", $name) if exists $self->{filebyname}{lc($name)}; $self->{filebyname}{lc($name)}= $file; $file->section_iterator( sub { my $section= shift; $self->markent($section->{ofs}, "s"); $section->nameiterator( sub { $self->markent($_[0], "n"); }, sub { $self->mark($_[0], $_[1], "n"); } ); $section->datatable_iterator( sub { $self->mark($_[0], $_[1], "a"); } ); if ($section->{indexblock}) { $self->mark($section->{indexblock}, $section->{indexsize}, "i"); } else { printf("NOTE: %s : %s is empty\n", $name, $section->{name}{name}); } } ); $file->nameiterator( sub { $self->markent($_[0], "N"); }, sub { $self->mark($_[0], $_[1], "N"); } ); $file->datatable_iterator( sub { $self->mark($_[0], $_[1], "A"); } ); if ($file->{indexblock} || $file->{indexsize}) { $self->mark($file->{indexblock}, $file->{indexsize}, "I"); } } ); return $self; } sub dumpinfo { my ($self)= @_; my %cxref; $cxref{substr($self->{chunkmap},$_,1)}++ for 0..length($self->{chunkmap})-1; printf("chunkmap: %s\n", join(", ", map { sprintf("%s:%d", $_, $cxref{$_}) } keys %cxref)); if ($self->{chunkmap} =~ /\s+$/ && length($&)!=$cxref{' '}) { printf(" %4d unallocated non-tail chunks\n", $cxref{' '}-length($&)); } print(join "", map { sprintf("%08x: %s\n", $_*0x100, substr($self->{chunkmap}, $_*0x100, 0x100)) } 0..int((length($self->{chunkmap})+0xff)/0x100)-1) if $::verbose; my %exref; $exref{substr($self->{entrymap},$_,1)}++ for 0..length($self->{entrymap})-1; printf("entrymap: %s\n", join(", ", map { sprintf("%s:%d", $_, $exref{$_}) } keys %exref)); print(join "", map { sprintf("%08x: %s\n", $_*0x100, substr($self->{entrymap}, $_*0x100, 0x100)) } 0..int((length($self->{entrymap})+0xff)/0x100)-1) if $::verbose; if ($self->{entrymap} =~ /\s+$/ && length($&)!=$exref{' '}) { printf(" %4d unallocated non-tail entries\n", $exref{' '}-length($&)); } printf("o2n: %d items, n2o: %d items, byname: %d items\n", scalar keys %{$self->{dir_ofs2nr}}, scalar keys %{$self->{dir_nr2ofs}}, scalar keys %{$self->{filebyname}}); return $self; } sub dirblock_iterator { my ($self, $blockfn)= @_; my $dirblockofs= $self->{imgfs}{firstdirofs}; #printf("dirblock iterator: %08lx\n", $dirblockofs); while ($dirblockofs) { my ($magic, $next)= unpack("VV", $self->{reader}->ReadData($dirblockofs, 8)); die sprintf("%08lx: invalid dirblock magic: %08lx\n", $dirblockofs, $magic) if $magic != 0x2f5314ce; $blockfn->($dirblockofs); $dirblockofs= $next; } } sub direntry_iterator { my ($self, $filefn)= @_; for my $dirblockofs (sort { $a<=>$b } keys %{$self->{dir_ofs2nr}}) { my $dirchunk = $self->{reader}->ReadData($dirblockofs+8, $self->{imgfs}{dirblocksize}-8); for (my $cofs= 0 ; $cofs+$self->{imgfs}{direntsize}<=length($dirchunk) ; $cofs += $self->{imgfs}{direntsize}) { my $magic= unpack("V", substr($dirchunk, $cofs, 4)); #printf("ent: %08lx : %s\n", $magic, unpack("H*", substr($dirchunk, $cofs+4,0x30))); if ($magic==0xfffffefe || $magic==0xfffff6fe) { $filefn->(FileEntry->load($self->{reader}, $dirblockofs+$cofs+8)); } } } } sub delete { my ($self, $name)= @_; return if !exists $self->{filebyname}{lc($name)}; my $file= $self->{filebyname}{lc($name)}; delete $self->{filebyname}{lc($name)}; printf("erasing index data\n") if $::verbose>1; $file->datatable_iterator( sub { printf("del datatabs: %08lx/%08lx\n", @_) if $::verbose>1; $self->erase($_[0], $_[1]); } ); printf("erasing index block\n") if $::verbose>1; $self->erase($file->{indexblock}, $file->{indexsize}); printf("erasing sections\n") if $::verbose>1; $file->section_iterator( sub { my $section= shift; printf("erasing section index data\n") if $::verbose>1; $section->datatable_iterator( sub { $self->erase($_[0], $_[1]); } ); printf("erasing section indexblock\n") if $::verbose>1; $self->erase($section->{indexblock}, $section->{indexsize}); printf("erasing section name\n") if $::verbose>1; $section->nameiterator( sub { printf("erasing section name entry\n") if $::verbose>1; $self->eraseent($_[0]); }, sub { printf("erasing section name chunk\n") if $::verbose>1; $self->erase($_[0], $_[1]); } ); printf("erasing section entry\n") if $::verbose>1; $self->eraseent($section->{ofs}); } ); printf("erasing file name\n") if $::verbose>1; $file->nameiterator( sub { printf("erasing file name entry\n") if $::verbose>1; $self->eraseent($_[0]); }, sub { printf("erasing file name chunk\n") if $::verbose>1; $self->erase($_[0], $_[1]); } ); printf("erasing file entry %s\n", $file->{name}{name}) if $::verbose>1; $self->eraseent($file->{ofs}); } sub rename { my ($self, $oldname, $newname)= @_; die sprintf("rename: %s does not exist\n", $oldname) if !exists $self->{filebyname}{lc($oldname)}; die sprintf("rename: %s already exists\n", $newname) if exists $self->{filebyname}{lc($newname)}; my $file= $self->{filebyname}{lc($oldname)}; delete $self->{filebyname}{lc($oldname)}; # first delete oldname $file->nameiterator( sub { printf("rename: erase entry %08lx\n", $_[0]) if $::verbose>1; $self->eraseent($_[0]); }, sub { printf("rename: erase chunk %08lx/%08lx\n", $_[0], $_[1]) if $::verbose>1; $self->erase($_[0], $_[1]); } ); $file->setname($newname, $self); $self->{filebyname}{lc($newname)}= $file; } sub dump { my ($self, $filename)= @_; if (!exists $self->{filebyname}{lc($filename)}) { die "$filename does not exist\n"; } $self->{filebyname}{lc($filename)}->dump(); } sub saveto { my ($self, $filename, $saveas)= @_; if (!exists $self->{filebyname}{lc($filename)}) { die "$filename does not exist\n"; } my $decomp= ($self->{imgfs}{compression} eq "XPR") ? \&XdaDevelopers::CompressUtils::XPR_DecompressDecode : ($self->{imgfs}{compression} eq "LZX") ? \&XdaDevelopers::CompressUtils::LZX_DecompressDecode : undef; $self->{filebyname}{lc($filename)}->saveto($saveas, $decomp); } sub addfile { my ($self, $filename, $data, $timestamp)= @_; my $file= FileEntry->new($self->{reader}); $file->{ofs}= $self->allocent("f"); $file->setname($filename, $self); $file->{size}= length($data); $file->{timestamp}= $timestamp; my @chunks; printf("alloc chunks\n") if $::verbose>1; my $o= 0; while ($o<$file->{size}) { my $wanted= ($file->{size}-$o<0x1000)?$file->{size}-$o:0x1000; my $udata= substr($data, $o, $wanted); # compress data my $cdata= ($self->{imgfs}{compression} eq "XPR") ? XdaDevelopers::CompressUtils::XPR_CompressEncode($udata) : ($self->{imgfs}{compression} eq "LZX") ? XdaDevelopers::CompressUtils::LZX_CompressEncode($udata) : undef; if (!defined $cdata || length($cdata) >= $wanted) { $cdata= $udata; } my $csize= length($cdata); push @chunks, { comp=>$csize, full=>$wanted, ofs=>$self->allocchunk($self->roundsize($csize), "A"), }; # pad last chunk with zero's if ($csize % $self->{imgfs}{chunksize}) { my $padlen= $self->{imgfs}{chunksize}-( $csize % $self->{imgfs}{chunksize} ); $cdata .= "\x00" x $padlen; } $self->{reader}->WriteData($chunks[-1]{ofs}, $cdata); $o+=$wanted; } my $indexdata= join("", map { pack("vvV", $_->{comp}, $_->{full}, $_->{ofs}) } @chunks); printf("alloc index (%d)\n", length($indexdata)) if $::verbose>1; my $roundedsize= $self->roundsize(length($indexdata)); $file->{indexblock}= $self->allocchunk($roundedsize, "I"); $file->{indexsize}= $roundedsize; printf("-> %08lx/%08lx\n", $file->{indexblock}, $file->{indexsize}) if $::verbose>1; $self->{reader}->WriteData($file->{indexblock}, $indexdata.("\x00" x ($roundedsize-length($indexdata)))); printf("save new file entry '%s'\n", $file->get_name()) if $::verbose>1; $file->save($file->{ofs}); } package FFFBFFFD; use strict; use IO::File; sub new { my ($class, $fh, $baseofs, $blocksize)= @_; my $self= bless { fh=>$fh, baseofs=>$baseofs, blocksize=>$blocksize, }, $class; return $self; } sub DESTROY { my ($self)= @_; return if (!$self->{maxwpos}); my $pos= $self->{maxwpos}; my $c0= $pos%0x800; if ($c0) { $pos += 0x800-$c0; } my $pofs= $self->cv2file($pos); $self->{fh}->seek($pofs-8, SEEK_SET); $self->{fh}->print(pack("VV", $self->vofs2physblocknr($pos-0x800), $self->{footinfo}{tag})); printf("lastblock: v=0x%x, p=0x%x\n", $pos, $pofs); } sub seek { my ($self, $pos, $set)= @_; if ($set == SEEK_SET) { $self->{pos}= $pos; } elsif ($set == SEEK_CUR) { $self->{pos}+= $pos; } elsif ($set == SEEK_END) { $self->{pos} = $self->filesize()+$pos; } return 1; } sub calcphyssize { my ($vpos, $vsize)= @_; my $psize= 0; my $c0= $vpos%0x800; if ($c0) { my $want= 0x800-$c0; $want=$vsize if ($want>$vsize); $psize += $want; $vpos += $want; $vsize -= $want; $psize += 8 if (($vpos%0x800)==0); } if ($vsize) { # now vpos is aligned to a 0x800 block my $nblocks= int($vsize/0x800); $psize += $nblocks*0x808; $vpos += $nblocks*0x800; $vsize -= $nblocks*0x800; } if ($vsize) { $psize += $vsize; $vpos += $vsize; $vsize -= $vsize; } return $psize; } sub read { my $self= shift; # $_[0] = data my $len= $_[1]; my $vofs= $self->{pos}; $_[0]= ""; my $physsize= calcphyssize($vofs, $len); my $pofs= $self->cv2file($vofs); $self->{fh}->seek($pofs, SEEK_SET); my $physdata; $self->{fh}->read($physdata, $physsize); $_[0]= ""; my $dofs= 0; while ($len) { my $want= 0x800-($vofs%0x800); $want= $len if ($want > $len); $_[0] .= substr($physdata, $dofs, $want); $dofs += $want+8; $len -= $want; $vofs += $want; # remember footer info if (!$self->{footinfo} && ($vofs%0x800)==0 && $dofs>=8 && length($physdata)>$dofs) { my @f= unpack("VV", substr($physdata, $dofs-8, 8)); if ($f[0]!=0xffffffff && $f[1]!=0xffffffff) { $self->{footinfo}= { tag=> $f[1], vblock=> $f[0], pblock=>($vofs/0x800)-1, }; printf("found foot reference: %08lx v=%d -> p=%d\n", $self->{footinfo}{tag}, $self->{footinfo}{vblock}, $self->{footinfo}{pblock}); } } } $self->{pos}= $vofs; return length($_[0]); } sub write { my $self= shift; my $vofs= $self->{pos}; my $pofs= $self->cv2file($vofs); my $physdata=""; if (($vofs%0x800)==0) { $physdata .= pack("VV", $self->vofs2physblocknr($vofs-0x800), $self->{footinfo}{tag}); $pofs -= 8; } my $len= length($_[0]); my $dofs= 0; while ($len) { my $want= $len; if ($want > 0x800-($vofs%0x800)) { $want= 0x800-($vofs%0x800); } $physdata .= substr($_[0], $dofs, $want); $dofs += $want; $len -= $want; $vofs += $want; if (($vofs%0x800)==0) { $physdata .= pack("VV", $self->vofs2physblocknr($vofs-0x800), $self->{footinfo}{tag}); } } $self->{fh}->seek($pofs, SEEK_SET); $self->{fh}->print($physdata); my $count= $vofs-$self->{pos}; $self->{pos}= $vofs; $self->{maxwpos}= $vofs if !defined $self->{maxwpos} || $self->{maxwpos}<$vofs; return $count; } sub vofs2physblocknr { my ($self, $vofs)= @_; return ($vofs/0x800)+$self->{footinfo}{vblock}-$self->{footinfo}{pblock}; } sub cv2file { my ($self, $ofs)= @_; # 000 -> 000 # 7ff -> 7ff # 800 -> 808 # 900 -> 908 # fff -> 1007 #1000 -> 1010 my $block= int($ofs/0x800); my $bofs= $ofs % 0x800; return $block*0x808+$bofs; } sub filesize { my ($self)= @_; return ($self->{fh}->filesize()/($self->{blocksize}+8))*$self->{blocksize}; } package main; use strict; use warnings; use Getopt::Long; use Carp; use IO::File; use integer; $|=1; my $reader; my @fileops; our $verbose=0; my $readonly; my $savedir; my $forcetyph; my %stats; my $imgfs; sub usage { return <<__EOF__ Usage: editimgfs file [edit commands] -add romname[=filename] -del romname -ren oldromname=newromname -dump romname -extract romname -d SAVEDIR - where to extract to -list __EOF__ } GetOptions( "add=s" => sub { my @args; if ($_[1] =~ /^\s*(.*?)=(.*?)\s*$/) { my ($romname, $filename)= ($1,$2); push @args, $romname, $filename; } elsif ($_[1] =~ /^\s*(.*?)\s*$/) { my ($romname, $filename)= ($1,$1); $romname =~ s{.*[/\\]}{}; push @args, $romname, $filename; } else { die "invalid add parameter: $_[1]\n"; } die "$args[1] does not exist\n" if ! -r $args[1]; push @fileops, { action=>\&del_file, param=>[ $args[0] ] }; push @fileops, { action=>\&add_file, param=>[ @args ] }; }, "ren=s" => sub { my @args; if ($_[1] =~ /^\s*(.*?)=(.*?)\s*$/) { push @args, $1, $2; } else { die "invalid ren parameter: $_[1]\n"; } push @fileops, { action=>\&ren_file, param=>[ @args ] }; }, "del=s" => sub { my @args; if ($_[1] =~ /^\s*(.*?)\s*$/) { push @args, $1; } else { die "invalid del parameter: $_[1]\n"; } push @fileops, { action=>\&del_file, param=>[ @args ] }; }, "dump=s" => sub { my @args; if ($_[1] =~ /^\s*(.*?)\s*$/) { push @args, $1; } else { die "invalid dump parameter: $_[1]\n"; } push @fileops, { action=>\&dump_file, param=>[ @args ] }; }, "extract=s" => sub { my @args; if ($_[1] =~ /^\s*(.*?)\s*$/) { push @args, $1; } else { die "invalid extract parameter: $_[1]\n"; } push @fileops, { action=>\&extract_file, param=>[ @args, $savedir || "." ] }; }, "list" => sub { push @fileops, { action=>\&list_files, param=>[] }; }, "v+" => \$verbose, "r" => \$readonly, "d=s" => \$savedir, "T" => \$forcetyph, ) or die usage(); sub read_file { my ($srcname)= @_; my $ifh= SimpleFile->new($srcname, "r") or die "$srcname: $!\n"; binmode $ifh; my $data; $ifh->read($data, $ifh->filesize()); $ifh->close(); return $data; } my $fn= shift or die usage(); my $fh= SimpleFile->new($fn, $readonly ? "r" : "r+") or croak "$fn: $!\n"; binmode $fh; my $imgfs_hdr= findimgfs_header($fh) || die "could not find imgfs header\n"; #print map { sprintf("%08lx %s\n", $imgfs_hdr->{$_}, $_) } keys %$imgfs_hdr; my $rd= $forcetyph || $imgfs_hdr->{readertype}==8 || $imgfs_hdr->{readertype}==0x20 ? TyphReader->new($imgfs_hdr->{fh}, $imgfs_hdr->{baseofs}) : $imgfs_hdr->{readertype}==0x40 ? HimaReader->new($imgfs_hdr->{fh}, $imgfs_hdr->{baseofs}) : undef; if (!$rd) { die sprintf("could not determine filetype: rd=0x%x base=0x%x\n", $imgfs_hdr->{readertype}, $imgfs_hdr->{baseofs}); } $imgfs= ImgfsFile->new($rd, $imgfs_hdr); printf("%d fileops\n", scalar @fileops) if $::verbose>1; for (@fileops) { $_->{action}->(@{$_->{param}}); } exit(0); sub findimgfs_header { my ($fh)= @_; # the imgfs header signature ( at start of imgfs partition ) my $signature= pack("H*", "f8ac2c9de3d42b4dbd30916ed84f31dc"); my $ofs= 0; while (1) { my $data; $fh->seek($ofs, SEEK_SET); $fh->read($data, 512) or last; my $i= index($data, $signature); if ($i>512-40) { warn sprintf("ignoring sig at %08lx\n", $ofs+$i); } elsif ($i>=0) { # the file we are processing starts with imgfs my @hdr= unpack("V7A4V2", substr($data, $i+16)); if ($hdr[3]==0x34 && ($hdr[7] eq "LZX" || $hdr[7] eq "XPR" || $hdr[7] eq "XPH")) { printf("found hdr at %08lx\n", $ofs+$i) if $::verbose>1; return { fh=>$fh, baseofs=>$ofs+$i, direntsize=>$hdr[3], # 0x1c dwBytesPerHeader readertype=>$hdr[4], # 0x20 dwChunksPerSector dirblocksize=>$hdr[5], # 0x24 dwFirstHeaderBlockOffset firstdirofs=>$hdr[5], # 0x24 dwFirstHeaderBlockOffset compression=>$hdr[7], # 0x2c zCompressionType freesectors=>$hdr[8], # 0x30 dwFreeSectorCount chunksperblock=>$hdr[4], chunksize=>0x40, entriesperblock=>int(($hdr[5]-8)/$hdr[3]), }; } else { printf("WARNING: sig at %08lx, but inv hdr: %08lx %s\n", $i+$ofs, $hdr[3], $hdr[7]); } } elsif (substr($data, 510,2) eq "\x55\xaa") { # partition table if (substr($data, 0x1e2,1) eq "\x25") { # imgfs partition my ($start, $size)= unpack("VV", substr($data, 0x1e6, 8)); $fh->read($data, 2048) or last; if (substr($data,0,8) eq "MSFLSH50") { $ofs= $start*0x200; next; } elsif (substr($data,0x200,8) eq "MSFLSH50") { $ofs= $start*0x400; next; } elsif (substr($data,0x600,8) eq "MSFLSH50") { $ofs= $start*0x800; next; } elsif (substr($data,0,16) eq "\x00\x00\x00\x00\xfd\xff\xfb\xffMSFLSH50") { $fh= FFFBFFFD->new($fh, 0, 0x800); $ofs= $start*0x200; next; } elsif (substr($data,0x200,16) eq "\x00\x00\x00\x00\xfd\xff\xfb\xffMSFLSH50") { $fh= FFFBFFFD->new($fh, 0, 0x800); $ofs= $start*0x400; next; } elsif (substr($data,0x600,16) eq "\x00\x00\x00\x00\xfd\xff\xfb\xffMSFLSH50") { $fh= FFFBFFFD->new($fh, 0, 0x800); $ofs= $start*0x800; next; } } } $ofs += 0x10000; } return undef; } #top level sub dump_file { my ($filename)= @_; $imgfs->dump($filename); } sub extract_file { my ($imgfsname, $targetpath)= @_; if (-d $targetpath) { $targetpath .= '/'.$imgfsname; } $imgfs->saveto($imgfsname, $targetpath); } sub add_file { my ($imgfsfile, $srcfile)= @_; if (my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks)= stat $srcfile) { my $data= read_file($srcfile); $imgfs->addfile($imgfsfile, $data, $mtime); } else { die "$srcfile: $!\n"; } } sub ren_file { my ($oldname, $newname)= @_; $imgfs->rename($oldname, $newname); } sub del_file { my ($filename)= @_; $imgfs->delete($filename); } sub list_files { $imgfs->dumpinfo(); for my $filename (keys %{$imgfs->{filebyname}}) { $imgfs->dump($filename); } }