from filetypes.base import *
import malcat as malcat
import math
import struct
from transforms import compress


def align(val, what, down=False):
    if val % what:
        if down:
            val -= val % what
        else:
            val += what - (val % what)
    return val

#https://dr-emann.github.io/squashfs/

META_TABLES = [
        ("Id", lambda sb: math.ceil(sb["IdCount"] / 2048)),
        ("Xattr", None), 
        ("Inode", None),
        ("Directory", None),
        ("Fragment", lambda sb: math.ceil(sb["FragmentCount"] / 512)),
        ("Export", lambda sb: math.ceil(sb["InodeCount"] / 1024))
]

class SuperBlock(Struct):

    def parse(self):
        yield String(4, zero_terminated=False, name="Magic")
        yield UInt32(name="InodeCount", comment="number of inodes stored in the inode table")
        yield Timestamp(name="ModificationTime", comment="when the archive was created (or last appended to)")
        bs = yield UInt32(name="BlockSize", comment="size of a data block in bytes. Must be a power of two between 4096 and 1048576 (1 MiB)")
        if bs % 4096:
            raise FatalError
        yield UInt32(name="FragmentCount", comment="number of entries in the fragment table")
        yield UInt16(name="Compression", comment="compression method used", values=[
            ("GZIP", 1),
            ("LZMA", 2),
            ("LZO", 3),
            ("XZ", 4),
            ("LZ4", 5),
            ("ZSTD", 6),
            ])
        bl = yield UInt16(name="BlockLog", comment="log2 of block_size. If block_size and block_log do not agree, the archive is considered corrupt")
        if bs != pow(2, bl):
            raise FatalError("Block log and block size do not agree")
        flags = yield BitsField(
            Bit(name="UncompressedInodes", comment="inodes are stored uncompressed. For backward compatibility reasons, UID/GIDs are also stored uncompressed"),
            Bit(name="UncompressedData", comment="data is stored uncompressed"),
            Bit(name="Check", comment="unused in squashfs 4+. Should always be unset"),
            Bit(name="UncompressedFragments", comment="fragments are stored uncompressed"),
            Bit(name="NoFragments", comment="fragments are not used. Files smaller than the block size are stored in a full block"),
            Bit(name="AlwaysFragments", comment="if the last block of a file is smaller than the block size, it will be instead stored as a fragment"),
            Bit(name="Duplicates", comment="identical files are recognized, and stored only once"),
            Bit(name="Exportable", comment="filesystem has support for export via NFS (the export table is populated)"),
            Bit(name="UncompressedXattrs", comment="Xattrs are stored uncompressed"),
            Bit(name="NoXattrs", comment="Xattrs are not stored"),
            Bit(name="CompressorOptions", comment="the compression options section is present"),
            Bit(name="UncompressedIds", comment="UID/GIDs are stored uncompressed. Note that the UNCOMPRESSED_INODES flag also has this effect. If that flag is set, this flag has no effect. This flag is currently only available on master in git, no released version of squashfs yet supports it"),
            name="Flags")
        yield UInt16(name="IdCount", comment="number of entries in the id lookup table")
        m = yield UInt16(name="VersionMajor", comment="major version of the squashfs file format. Should always equal 4")
        if m != 4:
            raise FatalError("Version")
        m = yield UInt16(name="VersionMinor", comment="major version of the squashfs file format. Should always equal 0")
        if m != 0:
            raise FatalError("Version")
        yield UInt64(name="RootInodeRef", comment="reference to the inode of the root directory of the archive")
        yield UInt64(name="BytesUsed", comment="number of bytes used by the archive. Because squashfs archives are often padded to 4KiB, this can often be less than the file size")
        for meta_name, _ in META_TABLES:
            yield Offset64(name="{}TableStart".format(meta_name), comment="byte offset at which the {} table starts".format(meta_name))


class MetadataBlock(Struct):

    def parse(self):
        sz = yield UInt16(name="Size")
        sz = sz & 0x7fff
        if sz:
            yield Bytes(sz, name="Data")


class Metadata(Struct):

    def __init__(self, *args, max_size=0, max_blocks=0, **kwargs):
        Struct.__init__(self, *args, **kwargs)
        self.max_size = max_size
        self.max_blocks = max_blocks
        if not (max_size or max_blocks):
            raise ValueError

    def parse(self):
        iblock = 0
        while True:
            mb = yield MetadataBlock(name="Block")
            iblock += 1
            if self.max_blocks and iblock >= self.max_blocks:
                break
            elif self.max_size and len(self) >= self.max_size:
                break

###################################################### COMPRESSION


class GzipCompressionOptions(Struct):

    def parse(self):
        yield UInt32(name="CompressionLevel", comment="should be in range [1,9]")
        yield UInt16(name="WindowSize", comment="should be in range [8,15]")
        yield BitsField(
            Bit(name="Default"),
            Bit(name="Filtered"),
            Bit(name="Huffman Only"),
            Bit(name="Run Length Encoded"),
            Bit(name="Fixed"),
            NullBits(9),
            name="Strategies")

class XzCompressionOptions(Struct):

    def parse(self):
        yield UInt32(name="DictionnarySize", comment="should be > 8KiB, and must be either the sum of a power of two, or the sum of two sequential powers of two")
        yield BitsField(
            Bit(name="x86"),
            Bit(name="powerpc"),
            Bit(name="ia64"),
            Bit(name="arm"),
            Bit(name="arthumb"),
            Bit(name="sparc"),
            NullBits(26),
            name="Filters")

class Lz4CompressionOptions(Struct):

    def parse(self):
        yield UInt32(name="Version", comment="only supported value is 1 (LZ4_LEGACY)")
        yield BitsField(
            Bit(name="HighCompressionMode"),
            NullBits(31),
            name="Flags")        

class ZstdCompressionOptions(Struct):

    def parse(self):
        yield UInt32(name="CompressionLevel", comment="should be in range [1,22]")        

class LzoCompressionOptions(Struct):

    def parse(self):
        yield UInt32(name="Algorithm", values=[
            ("lzo1x_1", 0),
            ("lzo1x_1_11", 1),
            ("lzo1x_1_12", 2),
            ("lzo1x_1_15", 3),
            ("lzo1x_999", 4),
            ])              
        yield UInt32(name="CompressionLevel", comment="compression level. For lzo1x_999, this can be a value between 0 and 9 (defaults to 8). Has to be 0 for all other algorithms")              



class NoDecompressor:
    def __init__(self, options=None):
        pass

    def decompress(data):
        return data

class GzipDecompressor:
    def __init__(self, options=None):
        self.transform = compress.ZlibDecompress()

    def __call__(self, data):
        return self.transform.run(data)

class LzmaDecompressor:
    def __init__(self, options=None):
        self.transform = compress.LzmaDecompress()

    def __call__(self, data):
        return self.transform.run(data, raw_mode=True)

class XzDecompressor:
    def __init__(self, options=None):
        self.transform = compress.LzmaDecompress()

    def __call__(self, data):
        return self.transform.run(data, raw_mode=False)    

class LzoDecompressor:
    def __init__(self, options=None):
        self.transform = compress.LzoDecompress()

    def __call__(self, data):
        return self.transform.run(data, header=False, buflen=1000000)


class CompressorOptionsAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "SquashFS.Comp"

    def parse(self, hint):
        compression = int(hint)
        optcls = {
            1: GzipCompressionOptions,
            3: LzoCompressionOptions,
            4: XzCompressionOptions,
            5: Lz4CompressionOptions,
            6: ZstdCompressionOptions,
        }.get(compression)
        if optcls is None:
            raise FatalError("No known compression option for {}".format(sb.Compression.enum))
        yield optcls(name="CompressionOptions", category=Type.HEADER)

###################################################### INODES

class BasicFile(Struct):

    def __init__(self, blocksize, **args):
        Struct.__init__(self, **args)
        self.blocksize = blocksize

    def parse(self):
        yield UInt32(name="BlockStart", comment="offset from the start of the archive where the data blocks are stored")
        fi = yield UInt32(name="FragmentBlockIndex", comment="index of a fragment entry in the fragment table which describes the data block the fragment of this file is stored in. If this file does not end with a fragment, this should be 0xFFFFFFFF")
        yield UInt32(name="BlockOffset", comment="(uncompressed) offset within the fragment data block where the fragment for this file")
        fs = yield UInt32(name="FileSize", comment="(uncompressed) size of file")
        num_block_entries = align(fs, self.blocksize, down = fi != 0xffffffff) // self.blocksize
        if num_block_entries:
            yield Array(num_block_entries, UInt32(), name="BlockSizes", comment="item in the list describes the (possibly compressed) size of a block")

class ExtendedFile(Struct):

    def __init__(self, blocksize, **args):
        Struct.__init__(self, **args)
        self.blocksize = blocksize

    def parse(self):
        yield UInt64(name="BlockStart", comment="offset from the start of the archive where the data blocks are stored")
        fs = yield UInt64(name="FileSize", comment="(uncompressed) size of file")
        yield UInt64(name="Sparse", comment="number of bytes saved by omitting blocks of zero bytes. Used in the kernel for sparse file accounting")
        yield UInt32(name="HardLinkCount", comment="number of hard links to this file")
        fi = yield UInt32(name="FragmentBlockIndex", comment="index of a fragment entry in the fragment table which describes the data block the fragment of this file is stored in. If this file does not end with a fragment, this should be 0xFFFFFFFF")
        yield UInt32(name="BlockOffset", comment="(uncompressed) offset within the fragment data block where the fragment for this file")
        yield UInt32(name="XattrIndex", comment="index into the xattr lookup table. Set to 0xFFFFFFFF if the inode has no extended attributes")
        num_block_entries = align(fs, self.blocksize, down = fi != 0xffffffff) // self.blocksize
        if num_block_entries:
            yield Array(num_block_entries, UInt32(), name="BlockSizes", comment="item in the list describes the (possibly compressed) size of a block")

class BasicSymlink(Struct):


    def parse(self):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this symlink")
        ts = yield UInt32(name="TargetSize", comment="size in bytes of the target_path this symlink points to")
        yield StringUtf8(ts, zero_terminated=False, name="TargetPath", comment="target path this symlink points to")


class ExtendedSymlink(Struct):

    def parse(self):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this symlink")
        ts = yield UInt32(name="TargetSize", comment="size in bytes of the target_path this symlink points to")
        yield StringUtf8(ts, zero_terminated=False, name="TargetPath", comment="target path this symlink points to")        
        yield UInt32(name="XattrIndex", comment="index into the xattr lookup table. Set to 0xFFFFFFFF if the inode has no extended attributes")


class BasicDirectory(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="BlockStart", comment="location of the of the block in the Directory Table where the directory entry information starts")
        yield UInt32(name="HardLinkCount", comment="number of hard links to this dir")
        yield UInt16(name="FileSize", comment="(uncompressed) size in bytes of the entries in the Directory Table, including headers plus 3. The extra 3 bytes are for a virtual '.' and '..' item in each directory which is not written, but can be considered to be part of the logical size of the directory")
        yield UInt16(name="BlockOffset", comment="(uncompressed) offset within the block in the Directory Table where the directory entry information starts")
        yield UInt32(name="ParentInodeId", comment="parent of this directory. If this is the root directory, this will be 1")

class DirectoryIndex(Struct):

    def parse(self):
        yield UInt32(name="Index", comment="byte offset from the first directory header to the current header, as if the uncompressed directory metadata blocks were laid out in memory consecutively")
        yield UInt32(name="Start", comment="start offset of a directory table metadata block")
        ns = yield UInt32(name="NameSize", comment="one less than the size of the entry name")
        yield StringUtf8(ns+1, zero_terminated=False, name="Name", comment="name of the first entry following the header without a trailing null byte")


class ExtendedDirectory(Struct):

    def parse(self):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this dir")
        yield UInt32(name="FileSize", comment="(uncompressed) size in bytes of the entries in the Directory Table, including headers plus 3. The extra 3 bytes are for a virtual '.' and '..' item in each directory which is not written, but can be considered to be part of the logical size of the directory")
        yield UInt32(name="BlockStart", comment="location of the of the block in the Directory Table where the directory entry information starts")
        yield UInt32(name="ParentInodeId", comment="parent of this directory. If this is the root directory, this will be 1")
        ic = yield UInt16(name="IndexCount", comment="number of directory index entries following the inode structure")
        yield UInt16(name="BlockOffset", comment="(uncompressed) offset within the block in the Directory Table where the directory entry information starts")
        yield UInt32(name="XattrIndex", comment="index into the xattr lookup table. Set to 0xFFFFFFFF if the inode has no extended attributes")
        for i in range(ic):
            yield DirectoryIndex()
    

class BasicDevice(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this inode")
        yield UInt32(name="Device", comment="major and minor deice number")


class ExtendedDevice(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this inode")
        yield UInt32(name="Device", comment="major and minor deice number")
        yield UInt32(name="XattrIndex", comment="index into the xattr lookup table. Set to 0xFFFFFFFF if the inode has no extended attributes")


class BasicFifo(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this inode")

class ExtendedFifo(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt32(name="HardLinkCount", comment="number of hard links to this inode")
        yield UInt32(name="XattrIndex", comment="index into the xattr lookup table. Set to 0xFFFFFFFF if the inode has no extended attributes")


class InodeHeader(Struct):

    #optim
    field_type = UInt16(name="Type", values = [
        ("BasicDirectory", 1),
        ("BasicFile", 2),
        ("BasicSymlink", 3),
        ("BasicBlockDevice", 4),
        ("BasicCharDevice", 5),
        ("BasicFifo", 6),
        ("BasicSocket", 7),
        ("ExtendedDirectory", 8),
        ("ExtendedFile", 9),
        ("ExtendedSymlink", 10),
        ("ExtendedBlockDevice", 11),
        ("ExtendedCharDevice", 12),
        ("ExtendedFifo", 13),
        ("ExtendedSocket", 14),
    ])

    fields_other = [
            UInt16(name="Permission", comment="bitmask representing the permissions for the item described by the inode. The values match with the permission values of mode_t (the mode bits, not the file type)"),
            UInt16(name="UidIndex", comment="index of the user id in the UID/GID Table"),
            UInt16(name="GidIndex", comment="index of the group id in the UID/GID Table"),
            Timestamp(name="ModifiedTime"),
            UInt32(name="InodeId", comment="position of this inode in the full list of inodes. Value should be in the range [1, inode_count](from the superblock) This can be treated as a unique identifier for this inode, and can be used as a key to recreate hard links"),
    ]

    def __init__(self, blocksize, **args):
        Struct.__init__(self, **args)
        self.blocksize = blocksize

    def parse(self):
        typ = yield InodeHeader.field_type
        for f in InodeHeader.fields_other:
            yield f
        if typ == 1:
            yield BasicDirectory()
        elif typ == 2:
            yield BasicFile(self.blocksize)
        elif typ == 3:
            yield BasicSymlink()
        elif typ == 4:
            yield BasicDevice(name="BasicBlockDevice")
        elif typ == 5:
            yield BasicDevice(name="BasicCharDevice")
        elif typ == 6:
            yield BasicFifo(name="BasicFifo")
        elif typ == 7:
            yield BasicFifo(name="BasicSocket")
        elif typ == 8:
            yield ExtendedDirectory()
        elif typ == 9:
            yield ExtendedFile(self.blocksize)
        elif typ == 10:
            yield ExtendedSymlink()
        elif typ == 11:
            yield ExtendedDevice(name="ExtendedBlockDevice")
        elif typ == 12:
            yield ExtendedDevice(name="ExtendedCharDevice")
        elif typ == 13:
            yield ExtendedFifo(name="ExtendedFifo")
        elif typ == 14:
            yield ExtendedFifo(name="ExtendedSocket")
        else:
            raise FatalError("Unknown inode type {}".format(typ))


class InodeTableAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "SquashFS.InodeTable"

    def parse(self, hint):
        self.blocksize, num_inodes = map(int, hint.split(";"))
        while self.remaining():
            hdr = yield InodeHeader(self.blocksize)

###################################################### DIRECTORIES


class DirectoryEntry(Struct):


    def parse(self):
        yield UInt16(name="Offset", comment="offset into the uncompressed inode metadata block")
        yield Int16(name="InodeDelta", comment="difference of this inode's number to the reference stored in the header")
        yield UInt16(name="InodeType", comment="inode type. For extended inodes, the corresponding basic type is stored here instead")
        ns = yield UInt16(name="NameSize")
        yield StringUtf8(ns+1, zero_terminated=False, name="Name", comment="Name of the file")


class DirectoryHeader(Struct):
    fields = [
        UInt32(name="Start", comment="starting byte offset of the block in the Inode Table where the inodes are stored"),
        UInt32(name="InodeBase", comment="arbitrary inode number. The entries that follow store their inode number as a difference to this"),
    ]    

    def parse(self):
        count = yield UInt32(name="Count", comment="one less than the number of entries following the header")
        for f in DirectoryHeader.fields:
            yield f
        for i in range(count+1):
            yield DirectoryEntry()



class DirectoryTableAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "SquashFS.DirectoryTable"

    def parse(self, hint):
        while self.remaining():
            hdr = yield DirectoryHeader()


###################################################### FRAGMENTS


class FragmentBlockEntry(StaticStruct):

    @classmethod
    def parse(cls):
        yield UInt64(name="FragmentStart", comment="offset within the archive where the fragment block starts")
        yield UInt32(name="Size", comment="stores two pieces of information. If the block is uncompressed, the 0x1000000 (1<<24) bit wil be set. The remaining bits describe the size of the fragment block on disk")
        yield Unused(4)


class FragmentTableAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "SquashFS.FragmentTable"

    def parse(self, hint):
        num_fragment_blocks = self.remaining() // 16
        yield Array(num_fragment_blocks, FragmentBlockEntry(), name="Fragments", category=Type.FIXUP)

            

###################################################### SQUASH

class FileInfo:

    def __init__(self, id, filesize):
        self.id = id
        self.size = filesize
        self.block_start = 0
        self.block_sizes = []
        self.fragment_index = None
        self.fragment_offset = 0


class SquashFSAnalyzer(FileTypeAnalyzer):
    category = malcat.FileType.FILESYSTEM
    name = "SquashFS"
    regexp = r"hsqs................[\x01-\x06]\x00"

    def __init__(self):
        FileTypeAnalyzer.__init__(self)
        self.metadata_blocks = {}
        self.blocks = {}
        self.fragments = []
        self.filesystem = {}
        self.decompressor = None


    def open_file(self, vfile, password=None):
        file_info = self.filesystem[vfile.path]
        data = bytearray()
        block_offset = file_info.block_start
        for blocksz in file_info.block_sizes:
            sz = blocksz & 0xFFFFFF
            uncompressed = (blocksz & 0x1000000) != 0
            if sz == 0:
                # sparse block
                read = b"\x00" * self.blocksize
            else:
                read = self.read(block_offset, sz)
                if not uncompressed:
                    read = self.decompressor(read)
            data.extend(read)
            block_offset += sz
        # final fragment
        if file_info.fragment_index is not None and file_info.size > len(data):
            data.extend(self.read_fragment(file_info.fragment_index, file_info.fragment_offset, file_info.size - len(data)))
        return data
            


    def open_meta(self, vfile, password=None):
        return self.read_metadata(self.metadata_blocks[vfile.path])


    def read_metadata(self, blocks_ids):
        res = bytearray()
        for block_id in blocks_ids:
            res.extend(self.read_block(self.blocks[block_id]))
        return res

    def read_block(self, block):
        sz = block["Size"]
        compressed = (sz & 0x8000) == 0
        res = block["Data"]
        if compressed:
            if self.decompressor is None:
                raise FatalError("Unknown compression method")
            res = self.decompressor(res)
        return res


    def read_data(self, block_address, block_offset, size, base=0):
        p = block_address + base
        res = bytearray()
        block = self.blocks.get(p)
        while size > 0 and block is not None:
            data = self.read_block(block)
            res.extend(data[block_offset:block_offset+size])
            block_offset = 0
            size -= len(data)
            p += block.size
            block = self.blocks.get(p)
        if size > 0:
            raise ValueError("Could not read {} bytes for block {:x}".format(size, p))
        return res

    def read_fragment(self, fragment_index, fragment_offset, size):
        if fragment_index >= len(self.fragments):
            raise KeyError(f"Unknown fragment id {fragment_index}")
        offset, sz, uncompressed = self.fragments[fragment_index]
        data = self.read(offset, sz)
        if not uncompressed:
            data = self.decompressor(data)
        if len(data) < fragment_offset + size:
            raise ValueError("Not enough bytes in fragment")
        return data[fragment_offset:fragment_offset + size]

    def parse(self, hint):
        sb = yield SuperBlock(category=Type.HEADER)
        self.blocksize = sb["BlockSize"]
        if sb["BytesUsed"] > self.size():
            print("Truncated archive")
        self.set_eof(sb["BytesUsed"])
        self.confirm()

        # compression options
        compression = sb["Compression"]
        compression_options = None
        if sb["Flags"]["CompressorOptions"]:
            md = yield Metadata(name="CompressorOptions", max_blocks=1, category=Type.HEADER)
            data = self.read_metadata(md)
            fake_file = malcat.FileBuffer(data, "CompressorOptions")
            parser = CompressorOptionsAnalyzer()
            parsed = parser.run(fake_file, hint=str(compression))
            compression_options = parsed[0]
            self.add_file("@CompressorOptions", len(data), "open_meta", "HSQS.Comp")
            
        decompressorcls = {
            1: GzipDecompressor,
            2: LzmaDecompressor,
            3: LzoDecompressor,
            4: XzDecompressor,
        }.get(compression)
        if decompressorcls is not None:
            self.decompressor = decompressorcls(compression_options)
        else:
            print("Unknown decompression {}".format(sb.Compression.enum))

        
        
        # parse indirect tables
        tables = []
        for i, pair in enumerate(META_TABLES):
            mn, indirect_fn = pair
            if indirect_fn is None: 
                continue
            table_start = getattr(sb, "{}TableStart".format(mn)).value
            if table_start == 0xffffffffffffffff:
                continue
            self.jump(table_start)
            count = indirect_fn(sb)
            blocks = yield Array(count, Offset64(), name="{}Table".format(mn))
            totsz = 0
            self.metadata_blocks["@{}Table".format(mn)] = []
            content = []
            for block_offset in blocks:
                block_offset = block_offset.value
                content.append(block_offset)
                block = self.blocks.get(block_offset)
                if block is None:
                    self.jump(block_offset)
                    block = yield MetadataBlock(name="{}TableBlock".format(mn), category=Type.DATA, parent=blocks)
                    self.blocks[block_offset] = block
                totsz += block.Data.size
            self.metadata_blocks["@{}Table".format(mn)] = content
            if totsz:
                self.add_file("@{}Table".format(mn), totsz, "open_meta", "SquashFS.{}Table".format(mn), f"{self.blocksize}")
        # parse direct tables
        for i, pair in enumerate(META_TABLES):
            mn, indirect_fn = pair
            if indirect_fn is not None: 
                continue
            table_start = getattr(sb, "{}TableStart".format(mn)).value
            if table_start == 0xffffffffffffffff:
                continue
            self.jump(table_start)
            next_table_start = self.size()
            for j in range(i+1, len(META_TABLES)):
                next_start = getattr(sb, "{}TableStart".format(META_TABLES[j][0])).value
                if next_start != 0xffffffffffffffff:
                    next_table_start = next_start
                    break
            available_space = min(self.size(), next_table_start) - self.tell()
            if available_space <= 0:
                continue
            for offset in self.blocks:
                if offset > table_start and offset < table_start + available_space:
                    available_space = offset - table_start
            md = yield Metadata(name="{}Table".format(mn), category=Type.DATA, max_size = available_space)
            sz = 0
            content = []
            for b in md:
                content.append(b.offset)
                self.blocks[b.offset] = b
                sz += b.Data.size
            cval = "{}Count".format(mn)
            hint = ""
            nodes_count = 0
            if cval in sb:
                nodes_count = sb[cval]
            self.add_file("@{}Table".format(mn), sz, "open_meta", "SquashFS.{}Table".format(mn), f"{self.blocksize};{nodes_count}")
            self.metadata_blocks["@{}Table".format(mn)] = content


        # fragments
        if "@FragmentTable" in self.metadata_blocks:
            data = self.read_metadata(self.metadata_blocks["@FragmentTable"])
            if data:
                fake_file = malcat.FileBuffer(data, "FragmentTable")
                parser = FragmentTableAnalyzer()
                parsed = parser.run(fake_file)
                parent = self["FragmentTable"]
                for i, f in enumerate(parsed["Fragments"]):
                    offset = f["FragmentStart"]
                    sz = f["Size"] &  0xFFFFFF
                    uncompressed = (f["Size"] & 0x1000000) != 0
                    self.fragments.append((offset, sz, uncompressed))
                    self.jump(offset)
                    yield Bytes(sz, name=f"FragmentTableBlock.{i}", category=Type.DATA, parent=parent)


        # inodes
        if "@InodeTable" in self.metadata_blocks and "@DirectoryTable" in self.metadata_blocks:
            # cache directory blocks
            directories_flat_file = bytearray()
            dir_offset_to_uncompressed_offset = {}
            dirstart = sb["DirectoryTableStart"]
            block_offset = dirstart
            uncompressed_offset = 0
            while block_offset in self.blocks:
                dir_offset_to_uncompressed_offset[block_offset - dirstart] = uncompressed_offset
                block = self.blocks[block_offset]
                block_content = self.read_block(block)
                uncompressed_offset += len(block_content)
                directories_flat_file.extend(block_content)
                block_offset += block.size

            inode2name_and_parent = {}
            directories = {}
            files = [] 
            data = self.read_metadata(self.metadata_blocks["@InodeTable"])
            if data:
                fake_file = malcat.FileBuffer(data, "InodeTable")
                parser = InodeTableAnalyzer()
                parsed = parser.run(fake_file, hint=f"{self.blocksize};{sb['InodeCount']}")
                real_inodes_count = 0
                for inode in parsed:
                    real_inodes_count += 1
                    inode_type = inode["Type"]
                    inode_id  = inode["InodeId"]
                    if inode_type in (1,):
                        # directory
                        if inode_type == 1:
                            dir_data = inode["BasicDirectory"]
                        else:
                            dir_data = inode["ExtendedDirectory"]
                        uncompressed_block_offset = dir_offset_to_uncompressed_offset.get(dir_data["BlockStart"])
                        start = uncompressed_block_offset + dir_data["BlockOffset"]
                        data = directories_flat_file[start: start + dir_data["FileSize"] - 3]
                        fake_file = malcat.FileBuffer(data, "DirectoryTable")
                        parser = DirectoryTableAnalyzer()
                        parsed = parser.run(fake_file, hint=f"{dir_data['FileSize']}")
                        for dirhdr in parsed:
                            base = dirhdr["InodeBase"]
                            for entry in dirhdr[3:]:
                                name = entry["Name"]
                                entry_inode_id = base + entry["InodeDelta"] 
                                #print(f"{inode_id} -> {entry_inode_id} ({name})")
                                inode2name_and_parent[entry_inode_id] = (name, inode_id)
                    elif inode_type in (2,):
                        if inode_type == 2:
                            file_data = inode["BasicFile"]
                        else:
                            file_data = inode["ExtendedFile"]
                        fi = FileInfo(inode_id, file_data["FileSize"])
                        if "BlockSizes" in file_data:
                            fi.block_start = file_data["BlockStart"]
                            fi.block_sizes = [x.value for x in file_data["BlockSizes"]]
                        fbi = file_data["FragmentBlockIndex"]
                        if fbi != 0xffffffff:
                            fi.fragment_index = fbi
                            fi.fragment_offset = file_data["BlockOffset"]
                        files.append(fi)
            if real_inodes_count != sb["InodeCount"]:
                print(f"Invalid number of inodes parsed: {real_inodes_count} vs {sb['InodeCount']} in theory")
            # get root inode
            root_inode_ref = sb["RootInodeRef"]
            block_offset = sb["InodeTableStart"] + (root_inode_ref >> 16) & 0xFFFFFFFF
            if not block_offset in self.blocks:
                raise FatalError("Cannot locate root inode block at {:x}".format(block_offset))
            content = self.read_block(self.blocks[block_offset])
            root_inode_id, = struct.unpack("<I", content[(root_inode_ref & 0xFFFF) + 0xc : (root_inode_ref & 0xFFFF) + 0xc + 4])

            # reconstruct FS
            def get_absolute_path(inode_id):
                name, parent = inode2name_and_parent[inode_id]
                if parent == root_inode_id:
                    return ["", name]
                else:
                    return get_absolute_path(parent) + [name]

            for file_info in files:
                try:
                    path = "/".join(get_absolute_path(file_info.id))
                    self.add_file(path, file_info.size, "open_file")
                    self.filesystem[path] = file_info
                except KeyError as e:
                    print(f"Can't reconstruct path for inode #{inode_id}")


