#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Webcamoid, webcam capture application.
# Copyright (C) 2017  Gonzalo Exequiel Pedone
#
# Webcamoid is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Webcamoid is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Webcamoid. If not, see <http://www.gnu.org/licenses/>.
#
# Web-Site: http://webcamoid.github.io/

import fnmatch
import os
import struct
import sys

import tools.binary


class DeployToolsBinary(tools.binary.DeployToolsBinary):
    def __init__(self):
        super().__init__()
        self.ldLibraryPath = os.environ['LD_LIBRARY_PATH'].split(':') if 'LD_LIBRARY_PATH' in os.environ else []
        self.libsSeachPaths = self.readLdconf() \
                            + ['/usr/lib',
                               '/usr/lib64',
                               '/lib',
                               '/lib64',
                               '/usr/local/lib',
                               '/usr/local/lib64']

    def readLdconf(self, ldconf='/etc/ld.so.conf'):
        if not os.path.exists(ldconf):
            return []

        confDir = os.path.dirname(ldconf)
        libpaths = []

        with open(ldconf) as f:
            for line in f:
                i = line.find('#')

                if i == 0:
                    continue

                if i >= 0:
                    line = line[: i]

                line = line.strip()

                if len(line) < 1:
                    continue

                if line.startswith('include'):
                    conf = line.split()[1]

                    if not conf.startswith('/'):
                        conf = os.path.join(confDir, conf)

                    dirname = os.path.dirname(conf)

                    if os.path.exists(dirname):
                        for f in os.listdir(dirname):
                            path = os.path.join(dirname, f)

                            if fnmatch.fnmatch(path, conf):
                                libpaths += self.readLdconf(path)
                else:
                    libpaths.append(line)

        return libpaths

    def isValid(self, path):
        with open(path, 'rb') as f:
            return f.read(4) == b'\x7fELF'

    def readString(self, f):
        s = b''

        while True:
            c = f.read(1)

            if c == b'\x00':
                break

            s += c

        return s

    def readNumber(self, f, arch):
        if arch == '32bits':
            return struct.unpack('I', f.read(4))[0]

        return struct.unpack('Q', f.read(8))[0]

    def readDynamicEntry(self, f, arch):
        if arch == '32bits':
            return struct.unpack('iI', f.read(8))

        return struct.unpack('qQ', f.read(16))

    # https://refspecs.linuxfoundation.org/lsb.shtml (See Core, Generic)
    # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
    def dump(self, binary):
        # ELF file magic
        ELFMAGIC = b'\x7fELF'

        # Sections
        SHT_STRTAB = 0x3
        SHT_DYNAMIC = 0x6

        # Dynamic section entries
        DT_NULL = 0
        DT_NEEDED = 1
        DT_RPATH = 15
        DT_RUNPATH = 0x1d

        with open(binary, 'rb') as f:
            # Read magic signature.
            magic = f.read(4)

            if magic != ELFMAGIC:
                return {}

            # Read the data structure of the file.
            eiClass = '32bits' if struct.unpack('B', f.read(1))[0] == 1 else '64bits'

            # Read machine code.
            f.seek(0x12, os.SEEK_SET)
            machine = struct.unpack('H', f.read(2))[0]

            # Get a pointer to the sections table.
            sectionHeaderTable = 0

            if eiClass == '32bits':
                f.seek(0x20, os.SEEK_SET)
                sectionHeaderTable = self.readNumber(f, eiClass)
                f.seek(0x30, os.SEEK_SET)
            else:
                f.seek(0x28, os.SEEK_SET)
                sectionHeaderTable = self.readNumber(f, eiClass)
                f.seek(0x3c, os.SEEK_SET)

            # Read the number of sections.
            nSections = struct.unpack('H', f.read(2))[0]

            # Read the index of the string table that stores sections names.
            shstrtabIndex = struct.unpack('H', f.read(2))[0]

            # Read sections.
            f.seek(sectionHeaderTable, os.SEEK_SET)
            neededPtr = []
            rpathsPtr = []
            runpathsPtr = []
            strtabs = []
            shstrtab = []

            for section in range(nSections):
                sectionStart = f.tell()

                # Read the a pointer to the virtual address in the string table
                # that contains the name of this section.
                sectionName = struct.unpack('I', f.read(4))[0]

                # Read the type of this section.
                sectionType = struct.unpack('I', f.read(4))[0]

                # Read the virtual address of this section.
                f.seek(sectionStart + (0x0c if eiClass == '32bits' else 0x10), os.SEEK_SET)
                shAddr = self.readNumber(f, eiClass)

                # Read the offset in file to this section.
                shOffset = self.readNumber(f, eiClass)
                f.seek(shOffset, os.SEEK_SET)

                if sectionType == SHT_DYNAMIC:
                    # Read dynamic sections.
                    while True:
                        # Read dynamic entries.
                        dTag, dVal = self.readDynamicEntry(f, eiClass)

                        if dTag == DT_NULL:
                            # End of dynamic sections.
                            break
                        elif dTag == DT_NEEDED:
                            # Dynamically imported libraries.
                            neededPtr.append(dVal)
                        elif dTag == DT_RPATH:
                            # RPATHs.
                            rpathsPtr.append(dVal)
                        elif dTag == DT_RUNPATH:
                            # RUNPATHs.
                            runpathsPtr.append(dVal)
                elif sectionType == SHT_STRTAB:
                    # Read string tables.
                    if section == shstrtabIndex:
                        # We found the string table that stores sections names.
                        shstrtab = [shAddr, shOffset]
                    else:
                        # Save string tables for later usage.
                        strtabs += [[sectionName, shAddr, shOffset]]

                # Move to next section.
                f.seek(sectionStart + (0x28 if eiClass == '32bits' else 0x40), os.SEEK_SET)

            # Libraries names and RUNPATHs are located in '.dynstr' table.
            strtab = []

            for tab in strtabs:
                f.seek(tab[0] - shstrtab[0] + shstrtab[1], os.SEEK_SET)

                if self.readString(f) == b'.dynstr':
                    strtab = tab

            # Read dynamically imported libraries.
            needed = set()

            for lib in neededPtr:
                f.seek(lib + strtab[2], os.SEEK_SET)
                needed.add(self.readString(f).decode(sys.getdefaultencoding()))

            # Read RPATHs
            rpaths = set()

            for path in rpathsPtr:
                f.seek(path + strtab[2], os.SEEK_SET)
                rpaths.add(self.readString(f).decode(sys.getdefaultencoding()))

            # Read RUNPATHs
            runpaths = set()

            for path in runpathsPtr:
                f.seek(path + strtab[2], os.SEEK_SET)
                runpaths.add(self.readString(f).decode(sys.getdefaultencoding()))

            return {'machine': machine,
                    'imports': needed,
                    'rpath': rpaths,
                    'runpath': runpaths}

        return {}

    def readRpaths(self, elfInfo, binDir):
        rpaths = []
        runpaths = []

        # http://amir.rachum.com/blog/2016/09/17/shared-libraries/
        for rpath in ['rpath', 'runpath']:
            for path in elfInfo[rpath]:
                if '$ORIGIN' in path:
                    path = path.replace('$ORIGIN', binDir)

                if not path.startswith('/'):
                    path = os.path.join(binDir, path)

                path = os.path.normpath(path)

                if rpath == 'rpath':
                    rpaths.append(path)
                else:
                    runpaths.append(path)

        return rpaths, runpaths

    def libPath(self, lib, machine, rpaths, runpaths):
        # man ld.so
        searchPaths = rpaths \
                    + self.ldLibraryPath \
                    + runpaths \
                    + self.libsSeachPaths

        for libdir in searchPaths:
            path = os.path.join(libdir, lib)

            if os.path.exists(path):
                depElfInfo = self.dump(path)

                if depElfInfo and depElfInfo['machine'] == machine:
                    return path

        return ''

    def dependencies(self, binary):
        elfInfo = self.dump(binary)

        if not elfInfo:
            return []

        rpaths, runpaths = self.readRpaths(elfInfo, os.path.dirname(binary))
        libs = []

        for lib in elfInfo['imports']:
            libpath = self.libPath(lib, elfInfo['machine'], rpaths, runpaths)

            if len(libpath) > 0 and not self.isExcluded(libpath):
                libs.append(libpath)

        return libs

    def name(self, binary):
        dep = os.path.basename(binary)[3:]

        return dep[: dep.find('.')]
