# Python Bindings for Librhash
# Copyright (c) 2011-2012, Sergey Basalaev <sbasalaev@gmail.com>
# Librhash is (c) 2011-2012, Aleksey Kravchenko <rhash.admin@gmail.com>
#
# 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.
#
# This library  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. Use it at your own risk!

"""Python bindings for librhash

Librhash  is  a library for computing and verifying hash sums
that supports many hashing algorithms. The  simplest  way  to
calculate  hash  of  a message  or  a file is by using one of
the functions:

hash_for_msg(message, hash_id)
hash_for_file(filename, hash_id)
magnet_for_file(filename, hash_ids)

Here  hash_id  is one of the constants CRC32, MD4, MD5, SHA1,
TIGER, TTH, BTIH, ED2K,  AICH,  WHIRLPOOL,  RIPEMD160,  GOST,
GOST_CRYPTOPRO, HAS160, SNEFRU128, SNEFRU256, SHA224, SHA256,
SHA384,  SHA512,   SHA3_224,  SHA3_256,  SHA3_384,  SHA3_512,
EDONR256 or EDONR512.  The first  two functions  will  return
the  default text  representation of  the message digest they
compute.  The latter will return magnet  link  for the  file.
In this function  you can  OR-combine  several hash_ids, like

>>> print magnet_for_file('rhash.py', CRC32 | MD5)
magnet:?xl=6041&dn=rhash.py&xt=urn:crc32:f5866f7a&xt=urn:md5:
f88e6c1620361da9d04e2a2a1c788059

Next, this module provides a class to calculate several hashes
simultaneously in an incremental way. Example of using it:

>>> hasher = RHash(CRC32 | MD5)
>>> hasher.update('Hello, ')
>>> hasher << 'world' << '!'
>>> hasher.finish()
>>> print hasher.HEX(CRC32)
EBE6C6E6
>>> print hasher.hex(MD5)
6cd3556deb0da54bca060b4c39479839

In this example RHash object is first created for  a  set  of
hashing algorithms. Then, data for hashing is given in chunks
with   methods   update(message)  and  update_file(filename).
Finally, call finish() to end up all remaining calculations.

To  receive  text represenation of the message digest use one
of the methods hex(), HEX(), base32(), BASE32(), base64() and
BASE64().  The hash() method  outputs  message digest  in its
default format.  Binary  message digest may be obtained  with
raw().  All of these  methods  accept  hash_id  as  argument.
It may  be omitted  if  RHash  was  created  to  compute hash
for only a single hashing algorithm.

Method  magnet(filename)  will generate magnet link with  all
hashes computed by the RHash object.
"""

# public API
__all__ = [
    'ALL', 'CRC32', 'MD4', 'MD5', 'SHA1', 'TIGER', 'TTH',
    'BTIH', 'ED2K', 'AICH', 'WHIRLPOOL', 'RIPEMD160', 'GOST',
    'GOST_CRYPTOPRO', 'HAS160', 'SNEFRU128', 'SNEFRU256',
    'SHA224', 'SHA256', 'SHA384', 'SHA512', 'EDONR256', 'EDONR512',
    'SHA3_224', 'SHA3_256', 'SHA3_384', 'SHA3_512',
    'RHash', 'hash_for_msg', 'hash_for_file', 'magnet_for_file']

import sys
from ctypes import (
    CDLL, c_char_p, c_int, c_size_t, c_uint, c_void_p, create_string_buffer)

# initialization
if sys.platform == 'win32':
    LIBNAME = 'librhash.dll'
elif sys.platform == 'darwin':
    LIBNAME = 'librhash.0.dylib'
elif sys.platform == 'cygwin':
    LIBNAME = 'cygrhash.dll'
elif sys.platform == 'msys':
    LIBNAME = 'msys-rhash.dll'
else:
    LIBNAME = 'librhash.so.0'
LIBRHASH = CDLL(LIBNAME)
LIBRHASH.rhash_library_init()

# function prototypes
LIBRHASH.rhash_init.argtypes = [c_uint]
LIBRHASH.rhash_init.restype = c_void_p
LIBRHASH.rhash_free.argtypes = [c_void_p]
LIBRHASH.rhash_reset.argtypes = [c_void_p]
LIBRHASH.rhash_update.argtypes = [c_void_p, c_char_p, c_size_t]
LIBRHASH.rhash_final.argtypes = [c_void_p, c_char_p]
LIBRHASH.rhash_print.argtypes = [c_char_p, c_void_p, c_uint, c_int]
LIBRHASH.rhash_print.restype = c_size_t
LIBRHASH.rhash_print_magnet.argtypes = [
    c_char_p, c_char_p, c_void_p, c_uint, c_int]
LIBRHASH.rhash_print_magnet.restype = c_size_t
LIBRHASH.rhash_transmit.argtypes = [c_uint, c_void_p, c_size_t, c_size_t]

# conversion of a string to binary data with Python 2/3 compatibility
if sys.version < '3':
    def _s2b(string):
        """Python 2: just return the string"""
        return string
    def _msg_to_bytes(msg):
        """convert the msg parameter to a string"""
        if isinstance(msg, str):
            return msg
        return str(msg)
else:
    import codecs
    def _s2b(string):
        """Python 3: convert the string to binary data"""
        return codecs.utf_8_encode(string)[0]
    def _msg_to_bytes(msg):
        """convert the msg parameter to binary data"""
        if isinstance(msg, bytes):
            return msg
        if isinstance(msg, str):
            return _s2b(msg)
        return _s2b(str(msg))

# hash_id values
CRC32 = 0x01
MD4 = 0x02
MD5 = 0x04
SHA1 = 0x08
TIGER = 0x10
TTH = 0x20
BTIH = 0x40
ED2K = 0x80
AICH = 0x100
WHIRLPOOL = 0x200
RIPEMD160 = 0x400
GOST = 0x800
GOST_CRYPTOPRO = 0x1000
HAS160 = 0x2000
SNEFRU128 = 0x4000
SNEFRU256 = 0x8000
SHA224 = 0x10000
SHA256 = 0x20000
SHA384 = 0x40000
SHA512 = 0x80000
EDONR256 = 0x100000
EDONR512 = 0x200000
SHA3_224 = 0x0400000
SHA3_256 = 0x0800000
SHA3_384 = 0x1000000
SHA3_512 = 0x2000000
ALL = SHA3_512*2 - 1


#rhash_print values
RHPR_RAW = 1
RHPR_HEX = 2
RHPR_BASE32 = 3
RHPR_BASE64 = 4
RHPR_UPPERCASE = 8
RHPR_NO_MAGNET = 0x20
RHPR_FILESIZE = 0x40

class RHash(object):
    'Incremental hasher'

    def __init__(self, hash_ids):
        if hash_ids == 0:
            self._ctx = None
            raise ValueError('Invalid argument')
        self._ctx = LIBRHASH.rhash_init(hash_ids)
        #switching off the autofinal feature
        LIBRHASH.rhash_transmit(5, self._ctx, 0, 0)

    def __del__(self):
        if self._ctx != None:
            LIBRHASH.rhash_free(self._ctx)

    def reset(self):
        """reset this object to initial state"""
        LIBRHASH.rhash_reset(self._ctx)
        return self

    def update(self, message):
        """update this object with new data chunk"""
        data = _msg_to_bytes(message)
        LIBRHASH.rhash_update(self._ctx, data, len(data))
        return self

    def __lshift__(self, message):
        return self.update(message)

    def update_file(self, filename):
        """Update this object with data from the given file."""
        file = open(filename, 'rb')
        buf = file.read(8192)
        while len(buf) > 0:
            self.update(buf)
            buf = file.read(8192)
        file.close()
        return self

    def finish(self):
        """Calculate hashes for all the data buffered by
        the update() method.
        """
        LIBRHASH.rhash_final(self._ctx, None)
        return self

    def _print(self, hash_id, flags):
        """Retrieve the message hash in required format."""
        buf = create_string_buffer(130)
        size = LIBRHASH.rhash_print(buf, self._ctx, hash_id, flags)
        if (flags & 3) == RHPR_RAW:
            return buf[0:size]
        else:
            return buf[0:size].decode()

    def raw(self, hash_id=0):
        """Returns the message hash as raw binary data."""
        return self._print(hash_id, RHPR_RAW)

    def hex(self, hash_id=0):
        """Returns the message hash as a hexadecimal lower-case string."""
        return self._print(hash_id, RHPR_HEX)

    def base32(self, hash_id=0):
        """Returns the message hash as a Base32 lower-case string."""
        return self._print(hash_id, RHPR_BASE32)

    def base64(self, hash_id=0):
        """Returns the message hash as a Base64 string."""
        return self._print(hash_id, RHPR_BASE64)

    # pylint: disable=invalid-name
    def HEX(self, hash_id=0):
        """Returns the message hash as a hexadecimal upper-case string."""
        return self._print(hash_id, RHPR_HEX | RHPR_UPPERCASE)

    def BASE32(self, hash_id=0):
        """Returns the message hash as a Base32 upper-case string."""
        return self._print(hash_id, RHPR_BASE32 | RHPR_UPPERCASE)
    # pylint: enable=invalid-name

    def magnet(self, filepath):
        """Returns magnet link with all hashes computed by
        this object."""
        size = LIBRHASH.rhash_print_magnet(
            None, _s2b(filepath), self._ctx, ALL, RHPR_FILESIZE)
        buf = create_string_buffer(size)
        LIBRHASH.rhash_print_magnet(
            buf, _s2b(filepath), self._ctx, ALL, RHPR_FILESIZE)
        return buf[0:size-1].decode('utf-8')

    def hash(self, hash_id=0):
        """Returns the message digest for the given hash function
        as a string in the default format."""
        return self._print(hash_id, 0)

    def __str__(self):
        return self._print(0, 0)

def hash_for_msg(message, hash_id):
    """Computes and returns the message digest (in its default format)
    of the message"""
    handle = RHash(hash_id)
    handle.update(message).finish()
    return str(handle)

def hash_for_file(filename, hash_id):
    """Computes and returns the message digest (in its default format)
    of the file content"""
    handle = RHash(hash_id)
    handle.update_file(filename).finish()
    return str(handle)

def magnet_for_file(filename, hash_mask):
    """Computes and returns the magnet link for the file."""
    handle = RHash(hash_mask)
    handle.update_file(filename).finish()
    return handle.magnet(filename)
