def __init__(self, filename, device_infos):
        self.device_infos = device_infos
        self.partition_table = None
        self.lockers = None
        self.iosVersion = 0
        self.hasMBR = False
        self.metadata_whitening = False
        self.filename = filename
        self.encrypted = device_infos["hwModel"] not in ["M68AP", "N45AP", "N82AP", "N72AP"]
        self.initGeometry(device_infos["nand"])
        
        if os.path.basename(filename).startswith("ce_"):
            self.image = NANDImageSplitCEs(os.path.dirname(filename), device_infos["nand"])
        elif filename == "remote":
            self.image = NANDRemote(self.pageSize, self.metaSize, self.pagesPerBlock, self.bfn)
        else:
            self.image = NANDImageFlat(filename, device_infos["nand"])
        
        s, page0 = self.readPage(0,0, boot=True)
        self.nandonly = (page0 != None) and page0.startswith("ndrG")
        if self.nandonly:
            self.encrypted = True
            self.partition_scheme = IOFlashPartitionScheme(self, page0)

        magics = ["DEVICEINFOBBT"]
        nandsig = None
        if page0 and page0[8:14] == "Darwin":
            print "Found old style signature", page0[:8]
            nandsig = page0
        else:
            magics.append("NANDDRIVERSIGN")

        sp0 = {}
        #sp0 = self.readSpecialPages(0, magics)
        #print "Found %s special pages in CE 0" % (", ".join(sp0.keys()))
        if not self.nandonly:
            print "Device does not boot from NAND (=> has a NOR)"
        
        vfltype = '1'   #use VSVFL by default
        if not nandsig:
            nandsig = sp0.get("NANDDRIVERSIGN")
        if not nandsig:
            print "NANDDRIVERSIGN not found, assuming metadata withening = %d" % self.metadata_whitening
        else:
            nSig, flags = struct.unpack("<LL", nandsig[:8])
            #assert nandsig[3] == chr(0x43)
            vfltype = nandsig[1]
            self.metadata_whitening = (flags & 0x10000) != 0
            print "NAND signature 0x%x flags 0x%x withening=%d, epoch=%s" % (nSig, flags, self.metadata_whitening, nandsig[0])

        if self.device_infos.has_key("lockers"):
            self.lockers = EffaceableLockers(self.device_infos.lockers.data)
        if self.nandonly:
            unit = self.findLockersUnit()
            if unit:
                self.lockers = EffaceableLockers(unit[0x40:])
                self.lockers.display()
                self.device_infos.lockers = plistlib.Data(unit[0x40:0x40+960])
                if not self.device_infos.has_key("lockers") or not self.device_infos.has_key("EMF") or self.device_infos.EMF == "00"*32:
                    self.device_infos.lockers = plistlib.Data(unit[0x40:0x40+960])
                    EMF = self.getEMF(device_infos["key89B"].decode("hex"))
                    dkey = self.getDKey(device_infos["key835"].decode("hex"))
                    self.device_infos.EMF = EMF.encode("hex")
                    self.device_infos.DKey = dkey.encode("hex")

            deviceuniqueinfo = sp0.get("DEVICEUNIQUEINFO")
            if not deviceuniqueinfo:
                print "DEVICEUNIQUEINFO not found"
            else:
                scfg = parse_SCFG(deviceuniqueinfo)
                #print "Found DEVICEUNIQUEINFO, serial number=%s" % scfg.get("SrNm","SrNm not found !")

        if self.ppn:
            if filename != "remote":
                print "Using PPN FTL"
                self.ftl = PPNFTL(self)
        elif vfltype == '0':
            print "Using legacy VFL"
            self.vfl = VFL(self)
            self.ftl = FTL(self, self.vfl)
        else:
            print "Using VSVFL"
            self.vfl = VSVFL(self)
            self.ftl = YAFTL(self.vfl)
class NAND(object):
    H2FMI_HASH_TABLE = gen_h2fmi_hash_table()
    
    def __init__(self, filename, device_infos):
        self.device_infos = device_infos
        self.partition_table = None
        self.lockers = None
        self.iosVersion = 0
        self.hasMBR = False
        self.metadata_whitening = False
        self.filename = filename
        self.encrypted = device_infos["hwModel"] not in ["M68AP", "N45AP", "N82AP", "N72AP"]
        self.initGeometry(device_infos["nand"])
        
        if os.path.basename(filename).startswith("ce_"):
            self.image = NANDImageSplitCEs(os.path.dirname(filename), device_infos["nand"])
        elif filename == "remote":
            self.image = NANDRemote(self.pageSize, self.metaSize, self.pagesPerBlock, self.bfn)
        else:
            self.image = NANDImageFlat(filename, device_infos["nand"])
        
        s, page0 = self.readPage(0,0, boot=True)
        self.nandonly = (page0 != None) and page0.startswith("ndrG")
        if self.nandonly:
            self.encrypted = True
            self.partition_scheme = IOFlashPartitionScheme(self, page0)

        magics = ["DEVICEINFOBBT"]
        nandsig = None
        if page0 and page0[8:14] == "Darwin":
            print "Found old style signature", page0[:8]
            nandsig = page0
        else:
            magics.append("NANDDRIVERSIGN")

        sp0 = {}
        #sp0 = self.readSpecialPages(0, magics)
        #print "Found %s special pages in CE 0" % (", ".join(sp0.keys()))
        if not self.nandonly:
            print "Device does not boot from NAND (=> has a NOR)"
        
        vfltype = '1'   #use VSVFL by default
        if not nandsig:
            nandsig = sp0.get("NANDDRIVERSIGN")
        if not nandsig:
            print "NANDDRIVERSIGN not found, assuming metadata withening = %d" % self.metadata_whitening
        else:
            nSig, flags = struct.unpack("<LL", nandsig[:8])
            #assert nandsig[3] == chr(0x43)
            vfltype = nandsig[1]
            self.metadata_whitening = (flags & 0x10000) != 0
            print "NAND signature 0x%x flags 0x%x withening=%d, epoch=%s" % (nSig, flags, self.metadata_whitening, nandsig[0])

        if self.device_infos.has_key("lockers"):
            self.lockers = EffaceableLockers(self.device_infos.lockers.data)
        if self.nandonly:
            unit = self.findLockersUnit()
            if unit:
                self.lockers = EffaceableLockers(unit[0x40:])
                self.lockers.display()
                self.device_infos.lockers = plistlib.Data(unit[0x40:0x40+960])
                if not self.device_infos.has_key("lockers") or not self.device_infos.has_key("EMF") or self.device_infos.EMF == "00"*32:
                    self.device_infos.lockers = plistlib.Data(unit[0x40:0x40+960])
                    EMF = self.getEMF(device_infos["key89B"].decode("hex"))
                    dkey = self.getDKey(device_infos["key835"].decode("hex"))
                    self.device_infos.EMF = EMF.encode("hex")
                    self.device_infos.DKey = dkey.encode("hex")

            deviceuniqueinfo = sp0.get("DEVICEUNIQUEINFO")
            if not deviceuniqueinfo:
                print "DEVICEUNIQUEINFO not found"
            else:
                scfg = parse_SCFG(deviceuniqueinfo)
                #print "Found DEVICEUNIQUEINFO, serial number=%s" % scfg.get("SrNm","SrNm not found !")

        if self.ppn:
            if filename != "remote":
                print "Using PPN FTL"
                self.ftl = PPNFTL(self)
        elif vfltype == '0':
            print "Using legacy VFL"
            self.vfl = VFL(self)
            self.ftl = FTL(self, self.vfl)
        else:
            print "Using VSVFL"
            self.vfl = VSVFL(self)
            self.ftl = YAFTL(self.vfl)

    def initGeometry(self, d):
        self.metaSize = d.get("meta-per-logical-page", 0)
        if self.metaSize == 0:
            self.metaSize = 12
        dumpedPageSize = d.get("dumpedPageSize", d["#page-bytes"] + self.metaSize + 8)
        self.dump_size=  d["#ce"] * d["#ce-blocks"] * d["#block-pages"] * dumpedPageSize
        self.totalPages = d["#ce"] * d["#ce-blocks"] * d["#block-pages"]
        nand_size = d["#ce"] * d["#ce-blocks"] * d["#block-pages"] * d["#page-bytes"]
        hsize = sizeof_fmt(nand_size)
        self.bfn = d.get("boot-from-nand", False)
        self.ppn = d.get("ppn-device", False)
        self.dumpedPageSize = dumpedPageSize
        self.pageSize = d["#page-bytes"]
        self.bootloaderBytes = d.get("#bootloader-bytes", 1536)
        self.logicalPageSize = d.get("logical-page-size", self.pageSize)
        self.emptyBootloaderPage = "\xFF" * self.bootloaderBytes
        self.blankPage = "\xFF" * self.pageSize
        self.nCEs =d["#ce"]
        self.blocksPerCE = d["#ce-blocks"]
        self.pagesPerBlock = d["#block-pages"]
        self.pagesPerCE = self.blocksPerCE * self.pagesPerBlock
        self.vendorType = d["vendor-type"]
        self.deviceReadId = d.get("device-readid", 0)
        self.banks_per_ce_vfl = d["banks-per-ce"]

        if self.ppn:
            self.slc_pages = d.get("slc-pages", 0)
            self.block_bits =  d.get("block-bits", 0)
            self.cau_bits = d.get("cau-bits", 0)
            self.page_bits = d.get("page-bits", 0)

        if d.has_key("metadata-whitening"):
            self.metadata_whitening = (d["metadata-whitening"].data == "\x01\x00\x00\x00")
        if nand_chip_info.has_key(self.deviceReadId):
            self.banks_per_ce_physical = nand_chip_info.get(self.deviceReadId)[7]
        elif self.ppn:
            self.banks_per_ce_physical = struct.unpack("<L", d["caus-ce"].data)[0]
        else:
            #raise Exception("Unknown deviceReadId %x" % self.deviceReadId)
            print "!!! Unknown deviceReadId %x, assuming 1 physical bank /CE, will probably fail" % self.deviceReadId
            self.banks_per_ce_physical = 1
        print "Chip id 0x%x banks per CE physical %d" % (self.deviceReadId, self.banks_per_ce_physical)
        self.blocks_per_bank = self.blocksPerCE / self.banks_per_ce_physical
        if self.blocksPerCE & (self.blocksPerCE-1) == 0:
            self.bank_address_space = self.blocks_per_bank
            self.total_block_space = self.blocksPerCE
        else:
            bank_address_space = next_power_of_two(self.blocks_per_bank)
            self.bank_address_space = bank_address_space
            self.total_block_space = ((self.banks_per_ce_physical-1)*bank_address_space) + self.blocks_per_bank
        self.bank_mask = int(math.log(self.bank_address_space * self.pagesPerBlock,2))
        print "NAND geometry : %s (%d CEs (%d physical banks/CE) of %d blocks of %d pages of %d bytes data, %d bytes metdata)" % \
            (hsize, self.nCEs, self.banks_per_ce_physical, self.blocksPerCE, self.pagesPerBlock, self.pageSize, d["meta-per-logical-page"])    
    
    def unwhitenMetadata(self, meta, pagenum):
        if len(meta) != 12:
            return None
        s = list(struct.unpack("<LLL", meta))
        for i in xrange(3):
            s[i] ^= NAND.H2FMI_HASH_TABLE[(i+pagenum) % len(NAND.H2FMI_HASH_TABLE)]
        return struct.pack("<LLL", s[0], s[1],s[2])
    
    def readBootPage(self, ce, page):
        s,d=self.readPage(ce, page, boot=True)
        if d:
            return d[:self.bootloaderBytes]
        else:
            #print "readBootPage %d %d failed" % (ce,page)
            return self.emptyBootloaderPage
    
    def readMetaPage(self, ce, block, page, spareType=SpareData):
        return self.readBlockPage(ce, block, page, META_KEY, spareType=spareType)
                
    def readBlockPage(self, ce, block, page, key=None, lpn=None, spareType=SpareData):
        assert page < self.pagesPerBlock
        b = block % self.blocks_per_bank
        bank_offset = self.bank_address_space * (block / self.blocks_per_bank)
        pn = (bank_offset + block % self.blocks_per_bank) * self.pagesPerBlock + page
        return self.readPage(ce, pn, key, lpn, spareType=spareType)
    
    def translateabsPage(self, page):
        return page % self.nCEs, page/self.nCEs
    
    def readAbsPage(self, page, key=None, lpn=None):
        return self.readPage(page % self.nCEs, page/self.nCEs, key, lpn)
    
    def readPage(self, ce, page, key=None, lpn=None, spareType=SpareData, boot=False):
        if ce > self.nCEs or page > self.pagesPerCE:
            #hax physical banking
            pass#raise Exception("CE %d Page %d out of bounds" % (ce, page))
        if self.ppn and self.filename != "remote":
            #undo slc bit
            zz = self.block_bits + self.cau_bits + self.page_bits
            page = page & ((1 << zz) - 1)
        if self.filename != "remote": #undo banking hax
            bank = (page & ~((1 << self.bank_mask) - 1)) >> self.bank_mask
            page2 = (page & ((1 << self.bank_mask) - 1))
            page2 = bank * (self.blocks_per_bank) * self.pagesPerBlock + page2
            spare, data = self.image.readPage(ce, page2, boot)
        else:
            spare, data = self.image.readPage(ce, page, boot)
        if not data:
            return None,None
        if self.metadata_whitening and spare != "\x00"*12 and len(spare) == 12:
            spare = self.unwhitenMetadata(spare, page)
        if spareType:
            spare = spareType.parse(spare)
        if key and self.encrypted:
            if lpn != None: pageNum = lpn#spare.lpn #XXX
            else:           pageNum = page
            return spare, self.decryptPage(data, key, pageNum)
        return spare, data

    def decryptPage(self, data, key, pageNum):
        if key == FILESYSTEM_KEY and self.ppn:
            return data
        return AESdecryptCBC(data, key, ivForPage(pageNum))
    
    def unpackSpecialPage(self, data):
        l = struct.unpack("<L", data[0x34:0x38])[0]
        return data[0x38:0x38 + l]
    
    def readSpecialPages(self, ce, magics):
        print "Searching for special pages..."
        specials = {}
        if self.nandonly:
            magics.append("DEVICEUNIQUEINFO")#, "DIAGCONTROLINFO")
        magics = map(lambda s: s.ljust(16,"\x00"), magics)

        lowestBlock = self.blocksPerCE - (self.blocksPerCE / 100)
        for block in xrange(self.blocksPerCE - 1, lowestBlock, -1):
            if len(magics) == 0:
                break
            #hax for physical banking
            bank_offset = self.bank_address_space * (block / self.blocks_per_bank)
            for page in xrange(self.pagesPerBlock-1,-1,-1):
                page = (bank_offset + block % self.blocks_per_bank) * self.pagesPerBlock + page
                s, data = self.readPage(ce, page)
                if data == None:
                    continue
                if data[:16] in magics:
                    self.encrypted = False
                    magics.remove(data[:16])
                    specials[data[:16].rstrip("\x00")] = self.unpackSpecialPage(data)
                    break
                data = self.decryptPage(data, META_KEY, page)
                #print data[:16]
                if data[:16] in magics:
                    #print data[:16], block, page
                    self.encrypted = True
                    magics.remove(data[:16])
                    specials[data[:16].rstrip("\x00")] = self.unpackSpecialPage(data)
                    break
        return specials

    def readLPN(self, lpn, key):
        return self.ftl.readLPN(lpn, key)
    
    def readVPN(self, vpn, key=None, lpn=None):
        return self.vfl.read_single_page(vpn, key, lpn)
    
    def dumpSystemPartition(self, outputfilename):
        return self.getPartitionBlockDevice(0).dumpToFile(outputfilename)

    def dumpDataPartition(self, emf, outputfilename):
        return self.getPartitionBlockDevice(1, emf).dumpToFile(outputfilename)

    def isIOS5(self):
        self.getPartitionTable()
        return self.iosVersion == 5
        
    def getPartitionTable(self):
        if self.partition_table:
            return self.partition_table
        pt = None
        key = FILESYSTEM_KEY if not self.ppn else None
        for i in xrange(10):
            d = self.readLPN(i, key)
            pt = parse_mbr(d)
            if pt:
                self.hasMBR = True
                self.iosVersion = 3
                break
            gpt = parse_gpt(d)
            if gpt:
                off = gpt.partition_entries_lba - gpt.current_lba
                d = self.readLPN(i+off, key)
                pt = GPT_partitions.parse(d)[:-1]
                self.iosVersion = 4
                break
            pt = parse_lwvm(d, self.logicalPageSize)
            if pt:
                self.iosVersion = 5
                break
        self.partition_table = pt
        return pt
    
    def getPartitionBlockDevice(self, partNum, key=None):
        pt = self.getPartitionTable()
        if self.hasMBR and pt[1].type == APPLE_ENCRYPTED and partNum == 1:
            data = self.readLPN(pt[1].last_lba - 1, FILESYSTEM_KEY)
            key = getEMFkeyFromCRPT(data, self.device_infos["key89B"].decode("hex"))
        if key == None:
            if partNum == 0:
                key = FILESYSTEM_KEY if not self.ppn else None
            elif partNum == 1 and self.device_infos.has_key("EMF"):
                key = self.device_infos["EMF"].decode("hex")
        return FTLBlockDevice(self, pt[partNum].first_lba, pt[partNum].last_lba, key)
    
    def getPartitionVolume(self, partNum, key=None):
        bdev = self.getPartitionBlockDevice(partNum, key)
        if partNum == 0:
            return HFSVolume(bdev)
        elif partNum == 1:
            self.device_infos["dataVolumeOffset"] = self.getPartitionTable()[partNum].first_lba
            return EMFVolume(bdev, self.device_infos)
    
    def findLockersUnit(self):
        if not self.nandonly:
            return
        for ce in xrange(0,self.nCEs):
            for block in xrange(4):#XXX: hax
                plog = self.partition_scheme.readPartitionBlock("plog", ce, block)
                for i in xrange(128):
                    d = plog[i*self.bootloaderBytes:(i+1)*self.bootloaderBytes]

                    if d and check_effaceable_header(d):
                        print "Found effaceable lockers in ce %d block %d (XXX possibly remapped) page %d" % (ce,block,i)
                        return d
   
    def getLockers(self):
        unit = self.findLockersUnit()
        if unit:
            return unit[0x40:0x40+960]

    def getEMF(self, k89b):
        return self.lockers.get_EMF(k89b)

    def getDKey(self, k835):
        return self.lockers.get_DKey(k835)

    def readBootPartition(self, block_start, block_end):
        res = ""
        for i in xrange(block_start*self.pagesPerBlock, block_end*self.pagesPerBlock):
            res += self.readBootPage(0, i)
        return res

    def get_img3s(self):
        if not self.nandonly:
            print "IMG3s are in NOR"
            return []
        blob = self.partition_scheme.readPartition("firm")
        hdr = IMG2.parse(blob[:0x100])
        i = hdr.images_block * hdr.block_size + hdr.images_offset
        img3s = extract_img3s(blob[i:i+hdr.images_length*hdr.block_size])
        
        boot = self.partition_scheme.readPartition("boot")
        img3s = extract_img3s(boot[0xc00:]) + img3s
        return img3s
        
    def extract_img3s(self, outfolder=None):
        if not self.nandonly:
            print "IMG3s are in NOR"
            return
        if outfolder:
            if self.filename != "remote": outfolder = os.path.join(os.path.dirname(self.filename), "img3")
            else: outfolder = os.path.join(".", "img3")
        if outfolder:
            makedirs(outfolder)
            print "Extracting IMG3s to %s" % outfolder
        for img3 in self.get_img3s():
            print img3.sigcheck(self.device_infos.get("key89A").decode("hex"))
            print img3.shortname
            if outfolder:
                write_file(outfolder+ "/%s.img3" % img3.shortname, img3.img3)
        return
        kernel = self.getPartitionVolume(0).readFile("/System/Library/Caches/com.apple.kernelcaches/kernelcache",returnString=True)
        if kernel:
            print "kernel"
            write_file(outfolder + "/kernelcache.img3", kernel)
            
    def extract_shsh(self, outfolder="."):
        if not self.nandonly:
            print "IMG3s are in NOR"
            return
        pass
    
    def getNVRAM(self):
        if not self.nandonly:
            print "NVRAM is in NOR"
            return 
        #TODO
        data = self.partition_scheme.readPartition("nvrm")

    def cacheData(self, name, data):
        if self.filename == "remote":
            return None
        save_pickle(self.filename + "." + name, data)
    
    def loadCachedData(self, name):
        try:
            if self.filename == "remote":
                return None
            return load_pickle(self.filename + "." + name)
        except:
            return None
        
    def dump(self, p):
        ioflash = IOFlashStorageKitClient()
        ioflash.dump_nand(p)