# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Low level interface - see UnRARDLL\UNRARDLL.TXT

from __future__ import generators

import ctypes, ctypes.wintypes
import os, os.path, sys, re
import Queue
import time

from rar_exceptions import *

ERAR_END_ARCHIVE = 10
ERAR_NO_MEMORY = 11
ERAR_BAD_DATA = 12
ERAR_BAD_ARCHIVE = 13
ERAR_UNKNOWN_FORMAT = 14
ERAR_EOPEN = 15
ERAR_ECREATE = 16
ERAR_ECLOSE = 17
ERAR_EREAD = 18
ERAR_EWRITE = 19
ERAR_SMALL_BUF = 20
ERAR_UNKNOWN = 21
ERAR_MISSING_PASSWORD = 22

RAR_OM_LIST = 0
RAR_OM_EXTRACT = 1

RAR_SKIP = 0
RAR_TEST = 1
RAR_EXTRACT = 2

RAR_VOL_ASK = 0
RAR_VOL_NOTIFY = 1

RAR_DLL_VERSION = 3

# enum UNRARCALLBACK_MESSAGES
UCM_CHANGEVOLUME = 0
UCM_PROCESSDATA = 1
UCM_NEEDPASSWORD = 2

architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
dll_name = "unrar.dll"
if architecture_bits == 64:
    dll_name = "x64\\unrar64.dll"
    
volume_naming1 = re.compile("\.r([0-9]{2})$")    
volume_naming2 = re.compile("\.([0-9]{3}).rar$")
volume_naming3 = re.compile("\.part([0-9]+).rar$")
    
try:
    unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
except WindowsError:
    unrar = ctypes.WinDLL(dll_name)


class RAROpenArchiveDataEx(ctypes.Structure):
    def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
        self.CmtBuf = ctypes.c_buffer(64*1024)
        ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))

    _fields_ = [
                ('ArcName', ctypes.c_char_p),
                ('ArcNameW', ctypes.c_wchar_p),
                ('OpenMode', ctypes.c_uint),
                ('OpenResult', ctypes.c_uint),
                ('_CmtBuf', ctypes.c_voidp),
                ('CmtBufSize', ctypes.c_uint),
                ('CmtSize', ctypes.c_uint),
                ('CmtState', ctypes.c_uint),
                ('Flags', ctypes.c_uint),
                ('Reserved', ctypes.c_uint*32),
               ]

class RARHeaderDataEx(ctypes.Structure):
    def __init__(self):
        self.CmtBuf = ctypes.c_buffer(64*1024)
        ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))

    _fields_ = [
                ('ArcName', ctypes.c_char*1024),
                ('ArcNameW', ctypes.c_wchar*1024),
                ('FileName', ctypes.c_char*1024),
                ('FileNameW', ctypes.c_wchar*1024),
                ('Flags', ctypes.c_uint),
                ('PackSize', ctypes.c_uint),
                ('PackSizeHigh', ctypes.c_uint),
                ('UnpSize', ctypes.c_uint),
                ('UnpSizeHigh', ctypes.c_uint),
                ('HostOS', ctypes.c_uint),
                ('FileCRC', ctypes.c_uint),
                ('FileTime', ctypes.c_uint),
                ('UnpVer', ctypes.c_uint),
                ('Method', ctypes.c_uint),
                ('FileAttr', ctypes.c_uint),
                ('_CmtBuf', ctypes.c_voidp),
                ('CmtBufSize', ctypes.c_uint),
                ('CmtSize', ctypes.c_uint),
                ('CmtState', ctypes.c_uint),
                ('Reserved', ctypes.c_uint*1024),
               ]

def DosDateTimeToTimeTuple(dosDateTime):
    """Convert an MS-DOS format date time to a Python time tuple.
    """
    dosDate = dosDateTime >> 16
    dosTime = dosDateTime & 0xffff
    day = dosDate & 0x1f
    month = (dosDate >> 5) & 0xf
    year = 1980 + (dosDate >> 9)
    second = 2*(dosTime & 0x1f)
    minute = (dosTime >> 5) & 0x3f
    hour = dosTime >> 11
    return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))

def _wrap(restype, function, argtypes):
    result = function
    result.argtypes = argtypes
    result.restype = restype
    return result

RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])

RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])

RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])

_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
def RARSetPassword(*args, **kwargs):
    _RARSetPassword(*args, **kwargs)

RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])

RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])

UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])



RARExceptions = {
                 ERAR_NO_MEMORY : MemoryError,
                 ERAR_BAD_DATA : ArchiveHeaderBroken,
                 ERAR_BAD_ARCHIVE : InvalidRARArchive,
                 ERAR_EOPEN : FileOpenError,
                }

class PassiveReader:
    """Used for reading files to memory"""
    def __init__(self, usercallback = None):
        self.buf = []
        self.ucb = usercallback
    
    def _callback(self, msg, UserData, P1, P2):
        if msg == UCM_PROCESSDATA:
            data = (ctypes.c_char*P2).from_address(P1).raw
            if self.ucb!=None:
                self.ucb(data)
            else:
                self.buf.append(data)
        return 1
    
    def get_result(self):
        return ''.join(self.buf)

class RarInfoIterator(object):
    def __init__(self, arc):
        self.arc = arc
        self.index = 0
        self.headerData = RARHeaderDataEx()
        self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
        if self.res in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
            raise IncorrectRARPassword
        self.arc.lockStatus = "locked"
        self.arc.needskip = False
        
    def __iter__(self):
        return self
        
    def next(self):
        if self.index>0:
            if self.arc.needskip:
                RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
            self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))

        if self.res:
            raise StopIteration 
        self.arc.needskip = True
        
        data = {}
        data['index'] = self.index
        data['filename'] = self.headerData.FileNameW
        data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
        data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
        data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
        if self.headerData.CmtState == 1:
            data['comment'] = self.headerData.CmtBuf.value
        else:
            data['comment'] = None
        self.index += 1
        return data

            
    def __del__(self):
        self.arc.lockStatus = "finished"

def generate_password_provider(password):
    def password_provider_callback(msg, UserData, P1, P2):
        if msg == UCM_NEEDPASSWORD and password!=None:
            (ctypes.c_char*P2).from_address(P1).value = password
        return 1
    return password_provider_callback

class RarFileImplementation(object):

    def init(self, password=None):
        self.password = password
        archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
        self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
        self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
        RARSetCallback(self._handle, self.c_callback, 1)

        if archiveData.OpenResult != 0:
            raise RARExceptions[archiveData.OpenResult]

        if archiveData.CmtState == 1:
            self.comment = archiveData.CmtBuf.value
        else:
            self.comment = None

        if password:
            RARSetPassword(self._handle, password)
            
        self.lockStatus = "ready"
        
        self.isVolume = archiveData.Flags & 1


    def destruct(self):
        if self._handle and RARCloseArchive:
            RARCloseArchive(self._handle)

    def make_sure_ready(self):
        if self.lockStatus == "locked":
            raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
        if self.lockStatus == "finished":
            self.destruct()
            self.init(self.password)

    def infoiter(self):
        self.make_sure_ready()
        return RarInfoIterator(self)

    def read_files(self, checker):
        res = []
        for info in self.infoiter():
            if checker(info) and not info.isdir:
                reader = PassiveReader()
                c_callback = UNRARCALLBACK(reader._callback)
                RARSetCallback(self._handle, c_callback, 1)
                tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
                if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
                    raise IncorrectRARPassword
                self.needskip = False
                res.append((info, reader.get_result()))
        return res
        

    def extract(self,  checker, path, withSubpath, overwrite):
        res = []
        for info in self.infoiter():
            checkres = checker(info)
            if checkres!=False and not info.isdir:
                if checkres==True:
                    fn = info.filename
                    if not withSubpath:
                        fn = os.path.split(fn)[-1]
                    target = os.path.join(path, fn)
                else:
                    raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"                    
                    target = checkres
                if overwrite or (not os.path.exists(target)):
                    tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
                    if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
                        raise IncorrectRARPassword

                self.needskip = False
                res.append(info)
        return res

    def get_volume(self):
        if not self.isVolume:
            return None
        headerData = RARHeaderDataEx()
        res = RARReadHeaderEx(self._handle, ctypes.byref(headerData))
        arcName = headerData.ArcNameW
        match3 = volume_naming3.search(arcName)
        if match3 != None:
            return int(match3.group(1)) - 1
        match2 = volume_naming3.search(arcName)
        if match2 != None:
            return int(match2.group(1))
        match1 = volume_naming1.search(arcName)
        if match1 != None:
            return int(match1.group(1)) + 1
        return 0
            


