#!/usr/bin/env python

# $Id: nfbfile.py.html,v 1.2 2004/09/28 10:29:27 john Exp $
#
# JW.

import struct, sys, os, zlib, time, warnings, types, string

__all__ = [ 'NfbInfo', 'NfbFile' ]

ftypetbl = {
   1: 'FILE',
   2: 'DIRECTORY',
}

class ucs2string:
   """Strings in Nokia files are a two byte per character encoding,
   probably UCS-2."""

   def __init__(self, data):
      self.__data = data

   def __len__(self):
      return len(self.__data) / 2

   def __str__(self):
      return self.convert()

   def __eq__(self, value):
      return value == str(self).encode('utf-8')

   def convert(self):
      """Parse the data into a unicode type string."""
      s = self.__data
      u = u''
      while s:
         c = s[:2]
         s = s[2:]
         c = ord(c[0]) | (ord(c[1])<<8)
         u += unichr(c)
      return u

   def write(self, fp):
      return fp.write(self.__data)


class IndexEntry:
   """FolderIndex or InfoIndex entry."""

   def __init__(self, line):
      data = line.split('\n')
      self.name = '\\'.join(data[0].split('\\v'))
      del(data[0])
      self.values = {}
      while len(data) >= 3:
         num = int(data[0])
         typ = data[1]
         val = data[2]
         if typ == 'long':
            val = long(val)
         elif typ == 'string':
            pass
         else:
            warnings.warn("Unknown Index value type `%s'" % typ)
         self.values[num] = val
         del(data[:3])

   def __repr__(self):
      s = '<IndexEntry: %s {' % self.name
      for k,v in self.values.iteritems():
         s += ' %s: %s'  % (k,v)
      return s + ' }>'

   def __getitem__(self, key):
      return self.values[key]


class IndexFile:
   """FolderIndex and InfoIndex file reading."""

   def __init__(self, data):
      assert(data[:4] == '2\r\n\n')
      self.version = int(data[:data.find('\r\n')])
      data = str(ucs2string(data[5:-2])).encode('utf-8').split('\n\n')
      self.rows = []
      for row in data:
         self.rows.append(IndexEntry(row))

   def __len__(self):
      return len(self.rows)

   def __getitem__(self, index):
      return self.rows[index]

   def __iter__(self):
      return iter(self.rows)


class InfoIndex(IndexFile):
   def __getitem__(self, key):
      index = -1
      if type(key) == types.StringType:
         if key[:7] == '\\FILES\\' and key[7] in string.digits:
            index = int(key[7:])
      elif type(key) == types.IntType:
         index = key
      return self.rows[index]


def readstring(fp, length=-1):
   """Read a UCS-2 string from the file fp."""
   if length == -1:
      length, = struct.unpack('L', fp.read(4))
   return ucs2string(fp.read(length * 2))

class NfbInfo:
   def __init__(self):
      self.filetype = ''
      self.filename = ''
      self.filesize = 0
      self.date_time = 0

   def __repr__(self):
      s = '<NfbInfo: %s; %s' % (self.filetype, self.filename)
      if self.filetype == 'FILE':
         s += '; %d' % self.filesize
         s += '; %s' % \
            time.strftime('%Y-%m-%d.%H:%M:%S', time.localtime(self.date_time))
      return s + '>'


class NfbFile:
   HiddenFiles = (
         '\\FILES',
         '\\FILES\\FolderIndex',
         '\\FILES\\InfoIndex',
         '\\FILES\\Language'
         )

   def __init__(self, filename, mode='r'):
      assert(mode == 'r')
      self.filename = filename
      self.mode = mode
      self.size = os.path.getsize(self.filename)
      self.fp = open(self.filename, self.mode)
      self.version, = struct.unpack('L', self.fp.read(4))
      assert(self.version == 3)
      self.fp.seek(0)
      length = self.size - 4
      crcvalue = 0
      bytes = 64*1024
      while length > 0:
         if bytes > length:
            bytes = length
         buf = self.fp.read(bytes)
         length -= len(buf)
         crcvalue = zlib.crc32(buf, crcvalue)
      self.crcvalue, = struct.unpack('l', self.fp.read(4))
      if long(crcvalue) != self.crcvalue:
         warnings.warn("CRC mismatch")
      self.fp.seek(4)
      self.firmware = readstring(self.fp)
      self.model = readstring(self.fp)
      self.entries, = struct.unpack('L', self.fp.read(4))
      self._startpos = self.fp.tell()
      self.folderindex = self.read('\\FILES\\FolderIndex')
      if self.folderindex:
         self.folderindex = IndexFile(self.folderindex)
      self.infoindex = self.read('\\FILES\\InfoIndex')
      if self.infoindex:
         self.infoindex = InfoIndex(self.infoindex)
      self.language = self.read('\\FILES\\Language')
      if self.language:
         self.language = str(ucs2string(self.language)).encode('utf-8')

   def _traverse(self, fn, user):
      self.fp.seek(self._startpos)
      for entry in xrange(self.entries):
         ftype, = struct.unpack('L', self.fp.read(4))
         path = str(readstring(self.fp)).encode('utf-8')

         if hasattr(self, 'infoindex') and self.infoindex \
               and path[:7] == '\\FILES\\' \
               and len(path) > 6 and path[7] in string.digits:
            info = self.infoindex[path]
            path = '\\FILES\\' + info.name
         else:
            info = None
         if ftype == 1:
            length, = struct.unpack('L', self.fp.read(4))
            offset = self.fp.tell()
            self.fp.seek(length, 1)
            timestamp, = struct.unpack('L', self.fp.read(4))
            if info:
               timestamp = info[0]
            fn(user, ftype, path, length, timestamp, offset)
         elif ftype == 2:
            fn(user, ftype, path)

   def _namelist(self, names, ftype, path, length=0, timestamp=0, offset=0):
      if path not in NfbFile.HiddenFiles:
         names.append(path)

   def namelist(self):
      names = []
      self._traverse(self._namelist, names)
      return names

   def _infolist(self, infos, ftype, path, length=0, timestamp=0, offset=0):
      if path not in NfbFile.HiddenFiles:
         info = NfbInfo()
         info.filetype = ftypetbl[ftype]
         info.filename = path
         if ftype == 1:
            info.filesize = length
            info.date_time = timestamp
         infos.append(info)

   def infolist(self):
      infos = []
      self._traverse(self._infolist, infos)
      return infos

   def _read(self, user, ftype, path, length=0, timestamp=0, offset=0):
      if user['name'] == path:
         user['ftype'] = ftype
         user['offset'] = offset
         user['length'] = length

   def _getinfo(self, user, ftype, path, length=0, timestamp=0, offset=0):
      if user['name'] == path:
         user['info'] = ( ftype, path, length, timestamp, offset )

   def getinfo(self, name):
      user = { 'name': name, 'info': None }
      self._traverse(self._getinfo, user)
      if user['info']:
         info = NfbInfo()
         ftype, path, length, timestamp, offset = user['info']
         info.filetype = ftypetbl[ftype]
         info.filename = path
         info.filesize = length
         info.date_time = timestamp
         return info
      return None

   def read(self, name):
      user = { 'name': name, 'offset': 0, 'ftype': 0, 'length': 0 }
      self._traverse(self._read, user)
      if user['offset'] == 0 or user['ftype'] != 1:
         return ''
      self.fp.seek(user['offset'])
      return self.fp.read(user['length'])

def usage(status):
   print >> sys.stderr, "Usage: NfbFile.py [-t|-x] [-v] NFB-FILE"
   sys.exit(status)

if __name__ == '__main__':
   import getopt

   opts, args = getopt.getopt(sys.argv[1:], 'txv?')

   mode = 'test'
   verbose = 0
   for opt,arg in opts:
      if opt == '-t':
         mode = 'test'
      elif opt == '-x':
         mode = 'extract'
      elif opt == '-v':
         verbose += 1
      elif opt == '-?':
         usage(0)

   if len(args) == 0:
      usage(1)

   filename = args[0]
   del(args[0])
   nfbfile = NfbFile(filename)
   if mode == 'test':
      print "Phone Firmware:", nfbfile.firmware
      print "Phone Model:", nfbfile.model
      if nfbfile.language:
         print "User file language:", nfbfile.language
      if verbose == 1:
         for name in nfbfile.namelist():
            print name
      elif verbose > 1:
         for info in nfbfile.infolist():
            print info
   elif mode == 'extract':
      if args:
         for arcname in args:
            arcname = arcname.replace('/', '\\')
            info = nfbfile.getinfo(arcname)
            if info:
               if os.isatty(sys.stdout.fileno()):
                  print info
                  print "(Direct stdout to a file to save content.)"
               else:
                  data = nfbfile.read(arcname)
                  sys.stdout.write(data)
            else:
               print "Not found", arcname