class HFSVolume(object): def __init__(self, bdev): self.bdev = bdev try: data = self.bdev.readBlock(0) self.header = HFSPlusVolumeHeader.parse(data[0x400:0x800]) assert self.header.signature == 0x4858 or self.header.signature == 0x482B except: raise #raise Exception("Not an HFS+ image") self.blockSize = self.header.blockSize self.bdev.setBlockSize(self.blockSize) #if os.path.getsize(filename) < self.header.totalBlocks * self.blockSize: # print "WARNING: HFS image appears to be truncated" self.allocationFile = HFSFile(self, self.header.allocationFile, kHFSAllocationFileID) self.allocationBitmap = self.allocationFile.readAllBuffer() self.extentsFile = HFSFile(self, self.header.extentsFile, kHFSExtentsFileID) self.extentsTree = ExtentsOverflowTree(self.extentsFile) self.catalogFile = HFSFile(self, self.header.catalogFile, kHFSCatalogFileID) self.xattrFile = HFSFile(self, self.header.attributesFile, kHFSAttributesFileID) self.catalogTree = CatalogTree(self.catalogFile, self) self.xattrTree = AttributesTree(self.xattrFile) self.hasJournal = self.header.attributes & (1 << kHFSVolumeJournaledBit) def readBlock(self, b): return self.bdev.readBlock(b) def writeBlock(self, lba, data): return self.bdev.writeBlock(lba, data) def volumeID(self): return struct.pack(">LL", self.header.finderInfo[6], self.header.finderInfo[7]) def isBlockInUse(self, block): thisByte = ord(self.allocationBitmap[block / 8]) return (thisByte & (1 << (7 - (block % 8)))) != 0 def unallocatedBlocks(self): for i in xrange(self.header.totalBlocks): if not self.isBlockInUse(i): yield i, self.read(i*self.blockSize, self.blockSize) def getExtentsOverflowForFile(self, fileID, startBlock, forkType=kForkTypeData): return self.extentsTree.searchExtents(fileID, forkType, startBlock) def getXattr(self, fileID, name): return self.xattrTree.searchXattr(fileID, name) def getFileByPath(self, path): return self.catalogTree.getRecordFromPath(path) def getFileIDByPath(self, path): key, record = self.catalogTree.getRecordFromPath(path) if not record: return if record.recordType == kHFSPlusFolderRecord: return record.data.folderID return record.data.fileID def listFolderContents(self, path): k,v = self.catalogTree.getRecordFromPath(path) if not k or v.recordType != kHFSPlusFolderRecord: return for k,v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r print v.data.folderID, getString(k).replace("\r","") + "/" elif v.recordType == kHFSPlusFileRecord: print v.data.fileID, getString(k) def ls(self, path): k,v = self.catalogTree.getRecordFromPath(path) return self._ls(k, v) def _ls(self, k, v): res = {} if not k or v.recordType != kHFSPlusFolderRecord: return None for k,v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r res[getString(k).replace("\r","") + "/"] = v.data elif v.recordType == kHFSPlusFileRecord: res[getString(k)] = v.data return res def listXattrs(self, path): k,v = self.catalogTree.getRecordFromPath(path) if k and v.recordType == kHFSPlusFileRecord: return self.xattrTree.getAllXattrs(v.data.fileID) elif k and v.recordType == kHFSPlusFolderThreadRecord: return self.xattrTree.getAllXattrs(v.data.folderID) def readFileByRecord(self, record): assert record.recordType == kHFSPlusFileRecord xattr = self.getXattr(record.data.fileID, "com.apple.decmpfs") data = None if xattr: decmpfs = HFSPlusDecmpfs.parse(xattr) if decmpfs.compression_type == 1: return xattr[16:] elif decmpfs.compression_type == 3: if decmpfs.uncompressed_size == len(xattr) - 16: return xattr[16:] return zlib.decompress(xattr[16:]) elif decmpfs.compression_type == 4: f = HFSCompressedResourceFork(self, record.data.resourceFork, record.data.fileID) data = f.readAllBuffer() return data f = HFSFile(self, record.data.dataFork, record.data.fileID) return f.readAllBuffer() #TODO: returnString compress def readFile(self, path, outFolder="./", returnString=False): k,v = self.catalogTree.getRecordFromPath(path) if not v: print "File %s not found" % path return assert v.recordType == kHFSPlusFileRecord xattr = self.getXattr(v.data.fileID, "com.apple.decmpfs") if xattr: decmpfs = HFSPlusDecmpfs.parse(xattr) if decmpfs.compression_type == 1: return xattr[16:] elif decmpfs.compression_type == 3: if decmpfs.uncompressed_size == len(xattr) - 16: z = xattr[16:] else: z = zlib.decompress(xattr[16:]) open(outFolder + os.path.basename(path), "wb").write(z) return elif decmpfs.compression_type == 4: f = HFSCompressedResourceFork(self, v.data.resourceFork, v.data.fileID) z = f.readAllBuffer() open(outFolder + os.path.basename(path), "wb").write(z) return z f = HFSFile(self, v.data.dataFork, v.data.fileID) if returnString: return f.readAllBuffer() else: f.readAll(outFolder + os.path.basename(path)) def readJournal(self): #jb = self.read(self.header.journalInfoBlock * self.blockSize, self.blockSize) #jib = JournalInfoBlock.parse(jb) #return self.read(jib.offset,jib.size) return self.readFile("/.journal", returnString=True) def listAllFileIds(self): self.fileids={} self.catalogTree.traverseLeafNodes(callback=self.grabFileId) return self.fileids def grabFileId(self, k,v): if v.recordType == kHFSPlusFileRecord: self.fileids[v.data.fileID] = True def getFileRecordForFileID(self, fileID): k,v = self.catalogTree.searchByCNID(fileID) return v def getFullPath(self, fileID): k,v = self.catalogTree.search((fileID, "")) if not k: print "File ID %d not found" % fileID return "" p = getString(v.data) while k: k,v = self.catalogTree.search((v.data.parentID, "")) if k.parentID == kHFSRootFolderID: break p = getString(v.data) + "/" + p return "/" + p def getFileRecordForPath(self, path): k,v = self.catalogTree.getRecordFromPath(path) if not k: return return v.data def getAllExtents(self, hfsplusfork, fileID): b = 0 extents = [] for extent in hfsplusfork.HFSPlusExtentDescriptor: extents.append(extent) b += extent.blockCount while b != hfsplusfork.totalBlocks: k,v = self.getExtentsOverflowForFile(fileID, b) if not v: print "extents overflow missing, startblock=%d" % b break for extent in v: extents.append(extent) b += extent.blockCount return extents def dohashFiles(self, k,v): if v.recordType == kHFSPlusFileRecord: filename = getString(k) f = HFSFile(self, v.data.dataFork, v.data.fileID) print filename, hashlib.sha1(f.readAllBuffer()).hexdigest() def hashFiles(self): self.catalogTree.traverseLeafNodes(callback=self.dohashFiles)
class HFSVolume(object): def __init__(self, bdev): self.bdev = bdev self.bdev.seek(0x400) self.header = HFSPlusVolumeHeader.parse_stream(self.bdev) assert self.header.signature == 0x4858 or self.header.signature == 0x482B self.blockSize = self.header.blockSize #if os.path.getsize(filename) < self.header.totalBlocks * self.blockSize: # print "WARNING: HFS image appears to be truncated" self.allocationFile = HFSFile(self, self.header.allocationFile, kHFSAllocationFileID) self.allocationBitmap = self.allocationFile.readAllBuffer() self.extentsFile = HFSFile(self, self.header.extentsFile, kHFSExtentsFileID) self.extentsTree = ExtentsOverflowTree(self.extentsFile) self.catalogFile = HFSFile(self, self.header.catalogFile, kHFSCatalogFileID) self.xattrFile = HFSFile(self, self.header.attributesFile, kHFSAttributesFileID) self.catalogTree = CatalogTree(self.catalogFile, self) self.xattrTree = AttributesTree(self.xattrFile) self.hasJournal = self.header.attributes & (1 << kHFSVolumeJournaledBit) k,v = self.catalogTree.search((kHFSRootFolderID, "")) self.volumename = getString(v.data) def readBlock(self, b): self.bdev.seek(b * self.blockSize) return self.bdev.read(self.blockSize) def writeBlock(self, lba, data): raise NotImplementedError # return self.bdev.writeBlock(lba, data) def volumeID(self): return struct.pack(">LL", self.header.finderInfo[6], self.header.finderInfo[7]) def isBlockInUse(self, block): thisByte = ord(self.allocationBitmap[block / 8]) return (thisByte & (1 << (7 - (block % 8)))) != 0 # def unallocatedBlocks(self): # for i in xrange(self.header.totalBlocks): # if not self.isBlockInUse(i): # yield i, self.read(i*self.blockSize, self.blockSize) def getExtentsOverflowForFile(self, fileID, startBlock, forkType=kForkTypeData): return self.extentsTree.searchExtents(fileID, forkType, startBlock) def getXattr(self, fileID, name): return self.xattrTree.searchXattr(fileID, name) def getFileByPath(self, path): return self.catalogTree.getRecordFromPath(path) def getFileIDByPath(self, path): key, record = self.catalogTree.getRecordFromPath(path) if not record: return if record.recordType == kHFSPlusFolderRecord: return record.data.folderID return record.data.fileID def listFolderContents(self, path): k,v = self.catalogTree.getRecordFromPath(path) if not k or v.recordType != kHFSPlusFolderRecord: return for k,v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r print v.data.folderID, getString(k).replace("\r","") + "/" elif v.recordType == kHFSPlusFileRecord: print v.data.fileID, getString(k) def ls(self, path): k,v = self.catalogTree.getRecordFromPath(path) return self._ls(k, v) def _ls(self, k, v): res = {} if not k or v.recordType != kHFSPlusFolderRecord: return None for k,v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r res[getString(k).replace("\r","") + "/"] = v.data elif v.recordType == kHFSPlusFileRecord: if is_hardlink(v.data): #print "hardlink iNode%d" % v.data.HFSPlusBSDInfo.special.iNodeNum k2,v2 = self.catalogTree.getRecordFromPath("/\x00\x00\x00\x00HFS+ Private Data/iNode%d" % v.data.HFSPlusBSDInfo.special.iNodeNum) res[getString(k)] = v2.data else: res[getString(k)] = v.data return res def listXattrs(self, path): k,v = self.catalogTree.getRecordFromPath(path) if k and v.recordType == kHFSPlusFileRecord: return self.xattrTree.getAllXattrs(v.data.fileID) elif k and v.recordType == kHFSPlusFolderThreadRecord: return self.xattrTree.getAllXattrs(v.data.folderID) def readCompressedFile(self, record, xattr, output): decmpfs = HFSPlusDecmpfs.parse(xattr) data = None if decmpfs.compression_type == 1: output.write(xattr[16:]) elif decmpfs.compression_type == 3: if decmpfs.uncompressed_size == len(xattr) - 16: output.write(xattr[16:]) elif xattr[16] == "\xFF": output.write(xattr[17:]) else: output.write(zlib.decompress(xattr[16:])) elif decmpfs.compression_type == 4: f = HFSCompressedResourceFork(self, record.data.resourceFork, record.data.fileID) f.readAllBuffer(output) def readFileByRecord(self, key, record, output): assert record.recordType == kHFSPlusFileRecord xattr = self.getXattr(record.data.fileID, "com.apple.decmpfs") if xattr: self.readCompressedFile(record, xattr, output) else: f = HFSFile(self, record.data.dataFork, record.data.fileID) f.readAll(output) return True def _readFile(self, path, output): k,v = self.catalogTree.getRecordFromPath(path) if not v: print "File %s not found" % path return assert v.recordType == kHFSPlusFileRecord return self.readFileByRecord(k, v, output) def readFile(self, path, outdir="./", returnString=False): if returnString: return self.readFileToString(path) outputfile = os.path.join(outdir,os.path.basename(path)) f = open(outputfile, "wb") res = self._readFile(path, f) f.close() if not res: os.unlink(outputfile) return res def readFileToString(self, path): sio = cStringIO.StringIO() self._readFile(path, sio) return sio.getvalue() def readJournal(self): #jb = self.read(self.header.journalInfoBlock * self.blockSize, self.blockSize) #jib = JournalInfoBlock.parse(jb) #return self.read(jib.offset,jib.size) return self.readFile("/.journal", returnString=True) def listAllFileIds(self): self.fileids={} self.catalogTree.traverseLeafNodes(callback=self.grabFileId) return self.fileids def grabFileId(self, k,v): if v.recordType == kHFSPlusFileRecord: self.fileids[v.data.fileID] = True def getFileRecordForFileID(self, fileID): k,v = self.catalogTree.searchByCNID(fileID) return v def getFullPath(self, fileID): k,v = self.catalogTree.search((fileID, "")) if not k: print "File ID %d not found" % fileID return "" if fileID == kHFSRootFolderID: return "/" p = getString(v.data) while k: k,v = self.catalogTree.search((v.data.parentID, "")) if k.parentID == kHFSRootFolderID: break p = getString(v.data) + "/" + p return "/" + p def getFileRecordForPath(self, path): k,v = self.catalogTree.getRecordFromPath(path) if not k: return return v.data def getAllExtents(self, hfsplusfork, fileID): b = 0 extents = [] for extent in hfsplusfork.HFSPlusExtentDescriptor: extents.append(extent) b += extent.blockCount while b != hfsplusfork.totalBlocks: k,v = self.getExtentsOverflowForFile(fileID, b) if not v: print "extents overflow missing, startblock=%d" % b break for extent in v: extents.append(extent) b += extent.blockCount return extents def dohashFiles(self, k,v): if v.recordType == kHFSPlusFileRecord and not is_symlink(v.data): filename = getString(k) f = HFSFile(self, v.data.dataFork, v.data.fileID) print filename, hashlib.sha1(f.readAllBuffer()).hexdigest() def hashFiles(self): self.catalogTree.traverseLeafNodes(callback=self.dohashFiles)
class HFSVolume(object): def __init__(self, bdev): self.bdev = bdev try: data = self.bdev.readBlock(0) self.header = HFSPlusVolumeHeader.parse(data[0x400:0x800]) assert self.header.signature == 0x4858 or self.header.signature == 0x482B except: raise #raise Exception("Not an HFS+ image") self.blockSize = self.header.blockSize self.bdev.setBlockSize(self.blockSize) #if os.path.getsize(filename) < self.header.totalBlocks * self.blockSize: # print "WARNING: HFS image appears to be truncated" self.allocationFile = HFSFile(self, self.header.allocationFile, kHFSAllocationFileID) self.allocationBitmap = self.allocationFile.readAllBuffer() self.extentsFile = HFSFile(self, self.header.extentsFile, kHFSExtentsFileID) self.extentsTree = ExtentsOverflowTree(self.extentsFile) self.catalogFile = HFSFile(self, self.header.catalogFile, kHFSCatalogFileID) self.xattrFile = HFSFile(self, self.header.attributesFile, kHFSAttributesFileID) self.catalogTree = CatalogTree(self.catalogFile, self) self.xattrTree = AttributesTree(self.xattrFile) self.hasJournal = self.header.attributes & ( 1 << kHFSVolumeJournaledBit) def readBlock(self, b): return self.bdev.readBlock(b) def writeBlock(self, lba, data): return self.bdev.writeBlock(lba, data) def volumeID(self): return struct.pack(">LL", self.header.finderInfo[6], self.header.finderInfo[7]) def isBlockInUse(self, block): thisByte = ord(self.allocationBitmap[block / 8]) return (thisByte & (1 << (7 - (block % 8)))) != 0 def unallocatedBlocks(self): for i in xrange(self.header.totalBlocks): if not self.isBlockInUse(i): yield i, self.read(i * self.blockSize, self.blockSize) def getExtentsOverflowForFile(self, fileID, startBlock, forkType=kForkTypeData): return self.extentsTree.searchExtents(fileID, forkType, startBlock) def getXattr(self, fileID, name): return self.xattrTree.searchXattr(fileID, name) def getFileByPath(self, path): return self.catalogTree.getRecordFromPath(path) def getFileIDByPath(self, path): key, record = self.catalogTree.getRecordFromPath(path) if not record: return if record.recordType == kHFSPlusFolderRecord: return record.data.folderID return record.data.fileID def listFolderContents(self, path): k, v = self.catalogTree.getRecordFromPath(path) if not k or v.recordType != kHFSPlusFolderRecord: return for k, v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r print v.data.folderID, getString(k).replace("\r", "") + "/" elif v.recordType == kHFSPlusFileRecord: print v.data.fileID, getString(k) def ls(self, path): k, v = self.catalogTree.getRecordFromPath(path) return self._ls(k, v) def _ls(self, k, v): res = {} if not k or v.recordType != kHFSPlusFolderRecord: return None for k, v in self.catalogTree.getFolderContents(v.data.folderID): if v.recordType == kHFSPlusFolderRecord: #.HFS+ Private Directory Data\r res[getString(k).replace("\r", "") + "/"] = v.data elif v.recordType == kHFSPlusFileRecord: res[getString(k)] = v.data return res def listXattrs(self, path): k, v = self.catalogTree.getRecordFromPath(path) if k and v.recordType == kHFSPlusFileRecord: return self.xattrTree.getAllXattrs(v.data.fileID) elif k and v.recordType == kHFSPlusFolderThreadRecord: return self.xattrTree.getAllXattrs(v.data.folderID) def readFileByRecord(self, record): assert record.recordType == kHFSPlusFileRecord xattr = self.getXattr(record.data.fileID, "com.apple.decmpfs") data = None if xattr: decmpfs = HFSPlusDecmpfs.parse(xattr) if decmpfs.compression_type == 1: return xattr[16:] elif decmpfs.compression_type == 3: if decmpfs.uncompressed_size == len(xattr) - 16: return xattr[16:] return zlib.decompress(xattr[16:]) elif decmpfs.compression_type == 4: f = HFSCompressedResourceFork(self, record.data.resourceFork, record.data.fileID) data = f.readAllBuffer() return data f = HFSFile(self, record.data.dataFork, record.data.fileID) return f.readAllBuffer() #TODO: returnString compress def readFile(self, path, outFolder="./", returnString=False): k, v = self.catalogTree.getRecordFromPath(path) if not v: print "File %s not found" % path return assert v.recordType == kHFSPlusFileRecord xattr = self.getXattr(v.data.fileID, "com.apple.decmpfs") if xattr: decmpfs = HFSPlusDecmpfs.parse(xattr) if decmpfs.compression_type == 1: return xattr[16:] elif decmpfs.compression_type == 3: if decmpfs.uncompressed_size == len(xattr) - 16: z = xattr[16:] else: z = zlib.decompress(xattr[16:]) open(outFolder + os.path.basename(path), "wb").write(z) return elif decmpfs.compression_type == 4: f = HFSCompressedResourceFork(self, v.data.resourceFork, v.data.fileID) z = f.readAllBuffer() open(outFolder + os.path.basename(path), "wb").write(z) return z f = HFSFile(self, v.data.dataFork, v.data.fileID) if returnString: return f.readAllBuffer() else: f.readAll(outFolder + os.path.basename(path)) def readJournal(self): #jb = self.read(self.header.journalInfoBlock * self.blockSize, self.blockSize) #jib = JournalInfoBlock.parse(jb) #return self.read(jib.offset,jib.size) return self.readFile("/.journal", returnString=True) def listAllFileIds(self): self.fileids = {} self.catalogTree.traverseLeafNodes(callback=self.grabFileId) return self.fileids def grabFileId(self, k, v): if v.recordType == kHFSPlusFileRecord: self.fileids[v.data.fileID] = True def getFileRecordForFileID(self, fileID): k, v = self.catalogTree.searchByCNID(fileID) return v def getFullPath(self, fileID): k, v = self.catalogTree.search((fileID, "")) if not k: print "File ID %d not found" % fileID return "" p = getString(v.data) while k: k, v = self.catalogTree.search((v.data.parentID, "")) if k.parentID == kHFSRootFolderID: break p = getString(v.data) + "/" + p return "/" + p def getFileRecordForPath(self, path): k, v = self.catalogTree.getRecordFromPath(path) if not k: return return v.data def getAllExtents(self, hfsplusfork, fileID): b = 0 extents = [] for extent in hfsplusfork.HFSPlusExtentDescriptor: extents.append(extent) b += extent.blockCount while b != hfsplusfork.totalBlocks: k, v = self.getExtentsOverflowForFile(fileID, b) if not v: print "extents overflow missing, startblock=%d" % b break for extent in v: extents.append(extent) b += extent.blockCount return extents def dohashFiles(self, k, v): if v.recordType == kHFSPlusFileRecord: filename = getString(k) f = HFSFile(self, v.data.dataFork, v.data.fileID) print filename, hashlib.sha1(f.readAllBuffer()).hexdigest() def hashFiles(self): self.catalogTree.traverseLeafNodes(callback=self.dohashFiles)