diff --git a/InvisibleCharm/__init__.py b/InvisibleCharm/__init__.py index abde520..8e68b93 100644 --- a/InvisibleCharm/__init__.py +++ b/InvisibleCharm/__init__.py @@ -8,7 +8,9 @@ from log21 import get_colors as _gc from InvisibleCharm.lib.Console import exit as _exit -__version__ = "2.2.0" +__version__ = "2.3.0" +__author__ = "CodeWriter21 (Mehrad Pooryoussof)" +__github__ = "Https://GitHub.com/MPCodeWriter21/InvisibleCharm" def entry_point(): diff --git a/InvisibleCharm/__main__.py b/InvisibleCharm/__main__.py index cfcbfd1..f8d287c 100644 --- a/InvisibleCharm/__main__.py +++ b/InvisibleCharm/__main__.py @@ -9,6 +9,9 @@ from InvisibleCharm.lib.Console import logger, input, verbose, quiet, exit from InvisibleCharm.lib.operations import win_embed, win_extract, win_attrib_hide, win_attrib_reveal, to_image_file, \ from_image_file, embed_file, extract_file +from InvisibleCharm.lib.Exceptions import CoverDataTypeNotFoundError, InvalidCoverDataTypeError, \ + NoEmbeddedFileFoundError +from InvisibleCharm.lib.File import get_names # Main function of script @@ -107,12 +110,26 @@ def main(): if args.cover: logger.warn(gc("lr") + ' ! Warning: ' + gc("blm", "gr") + "`to image` operation doesn't use cover file." + gc("rst")) - to_image_file(args.source, args.destination, args.delete, args.compress, args.encryption_pass, args.image_mode) + to_image_file(args.source, args.destination, args.delete, args.compress, args.encryption_pass, + args.image_mode) elif args.embed: if not args.cover or not os.path.exists(args.cover) or not os.path.isfile(args.cover): exit(gc("lr") + ' ! Error: Embed operation needs a cover file' + '\n + Source file must be an existing file!') - embed_file(args.source, args.cover, args.destination, args.delete, args.compress, args.encryption_pass) + try: + embed_file(args.source, args.cover, args.destination, args.delete, args.compress, args.encryption_pass) + except CoverDataTypeNotFoundError: + confirm = input(gc("lr") + f" ! Error: Couldn't identify cover file type!\n" + gc("ly") + + ' * Do you still want to use this cover file?' + gc("lw") + '(' + gc("ly") + + f'Enter {gc("lm")}Y{gc("ly")} to confirm{gc("lw")}){gc("lr")}: ' + + gc("lg")).lower() + if confirm == 'y': + embed_file(args.source, args.cover, args.destination, args.delete, args.compress, + args.encryption_pass, True) + else: + exit() + except InvalidCoverDataTypeError as ex: + exit('\r' + gc("lr") + " ! Error:", str(ex)) elif args.mode.lower() in ['reveal', 'r']: # Checks the chosen operation and calls the suitable function if args.cover: @@ -126,7 +143,22 @@ def main(): gc("ly") + f'Are you sure you want to remove source file({args.source})?(y/N) ' + gc("lg")) args.delete = answer == 'y' - win_extract(args.source, args.destination, args.delete, args.compress, args.encryption_pass) + # Gets the list of available embedded names in destination path + possible_names = get_names(args.source) + name = '' + if len(possible_names) == 0: + # Exits if no embedded file is available + exit(gc("lr") + f' ! Error: No win-embedded file found!') + elif len(possible_names) == 1: + # Choose the only embedded file automatically + name = possible_names[0] + else: + # Asks user to choose one of the embedded files + while name not in possible_names: + logger.print(gc("lb") + 'Available names' + gc("lr") + ': ' + gc("lg") + + f'{gc("lr")}, {gc("lg")}'.join(possible_names)) + name = input(gc("ly") + f'Enter the name of embedded file' + gc("lr") + ': ' + gc("lg")) + win_extract(args.source, args.destination, args.delete, args.compress, name, args.encryption_pass) elif args.win_attrib: if args.destination: logger.warn(gc("lr") + ' ! Warning: ' + gc("blm", "gr") + @@ -135,7 +167,10 @@ def main(): elif args.to_image: from_image_file(args.source, args.destination, args.delete, args.compress, args.encryption_pass) elif args.embed: - extract_file(args.source, args.destination, args.delete, args.compress, args.encryption_pass) + try: + extract_file(args.source, args.destination, args.delete, args.compress, args.encryption_pass) + except NoEmbeddedFileFoundError: + exit(gc("lr") + f' ! Error: No embedded file found!') else: exit(gc("lr") + f' ! Error: Mode: `{gc("lw")}{args.mode}{gc("lr")}` not found!') diff --git a/InvisibleCharm/lib/Console.py b/InvisibleCharm/lib/Console.py index 15fdc15..dddface 100644 --- a/InvisibleCharm/lib/Console.py +++ b/InvisibleCharm/lib/Console.py @@ -12,12 +12,25 @@ def input(*args, end='') -> str: + """ + Prints the input arguments and returns the user input. + + :param args: Input arguments to write in the console. + :param end: + :return: str + """ logger.info(*args, end=end) return _input_backup('') # Exits def exit(*args) -> None: + """ + Prints the input arguments and exits the program. + + :param args: Input arguments to write in the console. + :return: None + """ if args: logger.error(*args) logger.error(end='\033[0m') @@ -26,11 +39,21 @@ def exit(*args) -> None: # Enables verbose mode def verbose() -> int: + """ + Enables verbose mode. + + :return: int: logger.level + """ logger.setLevel(_DEBUG) return logger.level # Enables quiet mode def quiet() -> int: + """ + Enables quiet mode. + + :return: int: logger.level + """ logger.setLevel(_WARNING) return logger.level diff --git a/InvisibleCharm/lib/Exceptions.py b/InvisibleCharm/lib/Exceptions.py new file mode 100644 index 0000000..b176181 --- /dev/null +++ b/InvisibleCharm/lib/Exceptions.py @@ -0,0 +1,37 @@ +# InvisibleCharm.lib.Exceptions.py +# CodeWriter21 + +from zlib import error as _zlib_error +from magic.magic import MagicException as _MagicException + +__all__ = ['CouldNotDecompressError', 'InvalidCoverDataTypeError', 'CoverDataTypeNotFoundError', + 'NoEmbeddedDataFoundError', 'WinEmbeddedFileNotFoundError', 'NoEmbeddedFileFoundError', + 'NoWinEmbeddedFileFoundError'] + + +class CouldNotDecompressError(_zlib_error): + pass + + +class InvalidCoverDataTypeError(_MagicException): + pass + + +class CoverDataTypeNotFoundError(InvalidCoverDataTypeError): + pass + + +class NoEmbeddedDataFoundError: + pass + + +class NoEmbeddedFileFoundError(NoEmbeddedDataFoundError, FileNotFoundError): + pass + + +class WinEmbeddedFileNotFoundError(FileNotFoundError): + pass + + +class NoWinEmbeddedFileFoundError(WinEmbeddedFileNotFoundError, NoEmbeddedFileFoundError): + pass diff --git a/InvisibleCharm/lib/File.py b/InvisibleCharm/lib/File.py index bae864b..438155d 100644 --- a/InvisibleCharm/lib/File.py +++ b/InvisibleCharm/lib/File.py @@ -2,15 +2,38 @@ # CodeWriter21 import os as _os +from log21 import get_colors as _gc +from typing import Union as _Union, List as _List # We use getoutput function to get the output of a command from subprocess import getoutput as _getoutput -from log21 import get_colors as _gc from InvisibleCharm.lib.Console import logger as _logger from InvisibleCharm.lib.data.Prepare import prepare_data as _prepare_data, add_num as _add_num # Opens a file and returns prepared content -def open_file(path: str, name: str, hiding: bool = True, compress: bool = False, encrypt_pass: str = None) -> bytes: +def open_file(path: str, name: str, hiding: bool = True, compress: bool = False, + encrypt_pass: _Union[str, bytes] = '') -> bytes: + """ + Gets a file path and a name and reads the contents of the file and prepares it using + InvisibleCharm.lib.data.Prepare.prepare_data function. + + :param path: str: Input file path + :param name: str: A name to use for debug messages + :param hiding: bool = True: Are you going to hide this file? + :param compress: bool = False: Do you want to compress this file? + :param encrypt_pass: Union[str, bytes] = '': A password for encrypting the file + :return: bytes: Prepared data + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not _os.path.exists(path): + raise FileNotFoundError('`path` must be an existing file path.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + + name = str(name) _logger.debug(_gc("ly") + f' * Reading {name.lower()} file...', end='') with open(path, 'rb') as file: data = file.read() @@ -19,7 +42,23 @@ def open_file(path: str, name: str, hiding: bool = True, compress: bool = False, # Writes data in the given path -def save_file(path: str, data: bytes) -> None: +def save_file(path: str, data: _Union[bytes, str]) -> None: + """ + Writes data in the given path. + + :param path: str: File path to write the data. + :param data: Union[bytes, str]: Content to write. + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not isinstance(data, (str, bytes)): + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + if isinstance(data, str): + data = data.encode() + _logger.debug(_gc("ly") + ' * Writing in destination file...', end='') try: with open(path, 'wb') as dest_file: @@ -37,13 +76,39 @@ def save_file(path: str, data: bytes) -> None: # Deletes the file in the given path def delete_source_file(path: str) -> None: + """ + Deletes the file in the given path. + + :param path: str: File path to write the data. + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not _os.path.exists(path): + raise FileNotFoundError('`path` must be an existing file path.') + _logger.debug(_gc("ly") + ' * Deleting source file...', end='') _os.remove(path) _logger.debug('\r' + _gc("ly") + ' = Source file deleted') -# Returns the possible names of the embedded files in a windows path -def get_names(path: str) -> list: +# Returns the possible names of the embedded files in a Windows path +def get_names(path: str) -> _List[str]: + """ + Returns the possible names of the embedded files in a Windows path. + + :param path: str: File path to write the data. + :return: List[str] + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not _os.path.exists(path): + raise FileNotFoundError('`path` must be an existing file path.') + output = _getoutput('dir ' + _os.path.split(path)[0] + ' /r /a') names = [] for line in output.split('\n'): diff --git a/InvisibleCharm/lib/data/Encryption.py b/InvisibleCharm/lib/data/Encryption.py index f1cdc64..f6ff192 100644 --- a/InvisibleCharm/lib/data/Encryption.py +++ b/InvisibleCharm/lib/data/Encryption.py @@ -3,19 +3,52 @@ # We use hashlib library to hash passwords import hashlib +from log21 import get_colors as _gc +from typing import Union as _Union, Tuple as _Tuple # We use _AES to encrypt and decrypt data from Crypto.Cipher import AES as _AES -from log21 import get_colors as _gc from InvisibleCharm.lib.Console import logger as _logger __all__ = ['encrypt', 'decrypt'] -# Encrypts data using AES and costume password -def encrypt(data: bytes, password: str) -> bytes: +def __prepare(data: _Union[str, bytes], password: _Union[str, bytes]) -> _Tuple[bytes, bytes, bytes]: + """ + Prepares data for encrypt and decrypt functions. + + :param data: Union[str, bytes] + :param password: Union[str, bytes] + :return: Tuple[bytes, bytes, bytes]: data, md5_password_hash, sha512_password_hash + """ + # Checks whether the inputs are valid + if not isinstance(data, (str, bytes)): + raise TypeError('`data` must be an instance of str or bytes.') + if isinstance(data, str): + data = data.encode() + if not isinstance(password, (str, bytes)): + raise TypeError('`password` must be an instance of str or bytes.') + if isinstance(password, str): + password = password.encode() + _logger.debug(_gc("ly") + ' * Hashing password...') - md5 = hashlib.md5(password.encode()).digest() - sha512 = hashlib.sha512(password.encode()).digest() + md5 = hashlib.md5(password).digest() + sha512 = hashlib.sha512(password).digest() + + return data, md5, sha512 + + +# Encrypts data using AES and costume password +def encrypt(data: _Union[str, bytes], password: _Union[str, bytes]) -> bytes: + """ + Encrypts data using AES and costume password. + + :param data: Union[str, bytes]: Data to encrypt. + :param password: Password for encryption. + :return: bytes: Encrypted data. + """ + + data, md5, sha512 = __prepare(data, password) + _logger.debug(_gc("ly") + ' * Encrypting data...', end='') cipher = _AES.new(md5, _AES.MODE_EAX, nonce=sha512) data = cipher.encrypt(data) @@ -24,10 +57,17 @@ def encrypt(data: bytes, password: str) -> bytes: # Decrypts data using AES and costume password -def decrypt(data: bytes, password: str) -> bytes: - _logger.debug(_gc("ly") + ' * Hashing password...') - md5 = hashlib.md5(password.encode()).digest() - sha512 = hashlib.sha512(password.encode()).digest() +def decrypt(data: _Union[str, bytes], password: _Union[str, bytes]) -> bytes: + """ + Decrypts data using AES and costume password. + + :param data: Union[str, bytes]: Data to decrypt. + :param password: Password for decryption. + :return: bytes: Decrypted data. + """ + + data, md5, sha512 = __prepare(data, password) + _logger.debug(_gc("ly") + ' * Decrypting data...', end='') cipher = _AES.new(md5, _AES.MODE_EAX, nonce=sha512) data = cipher.decrypt(data) diff --git a/InvisibleCharm/lib/data/Prepare.py b/InvisibleCharm/lib/data/Prepare.py index cd09d18..53caafe 100644 --- a/InvisibleCharm/lib/data/Prepare.py +++ b/InvisibleCharm/lib/data/Prepare.py @@ -1,18 +1,38 @@ # InvisibleCharm.lib.data.Prepare.py # CodeWriter21 + import os as _os +from typing import Union as _Union from log21 import get_colors as _gc # We use zlib library to compress and decompress data import zlib as _zlib -from InvisibleCharm.lib.Console import logger as _logger, exit +from InvisibleCharm.lib.Console import logger as _logger +from InvisibleCharm.lib.Exceptions import CouldNotDecompressError as _CouldNotDecompressError from InvisibleCharm.lib.data.Encryption import encrypt as _encrypt, decrypt as _decrypt __all__ = ['prepare_data', 'add_num'] # Prepares data(compression and encryption) -def prepare_data(data: bytes, hiding: bool = True, compress: bool = False, encrypt_pass: str = None) -> bytes: +def prepare_data(data: _Union[str, bytes], hiding: bool = True, compress: bool = False, + encrypt_pass: _Union[str, bytes] = '') -> bytes: + """ + Prepares data(compression and encryption) + + :param data: Union[str, bytes]: Data to prepare. + :param hiding: bool = True: Are you going to hide this data? + :param compress: bool = False: Do you want to compress this file? + :param encrypt_pass: Union[str, bytes] = '': A password for encrypting the data. + :return: bytes: Prepared data. + """ + + # Checks whether the inputs are valid + if not isinstance(data, (str, bytes)): + raise TypeError('`data` must be an instance of str or bytes.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + if hiding: if compress: _logger.debug(_gc("ly") + ' * Compressing data...', end='') @@ -29,13 +49,25 @@ def prepare_data(data: bytes, hiding: bool = True, compress: bool = False, encry data = _zlib.decompress(data) _logger.debug('\r' + _gc("lg") + ' = Data decompressed.') except _zlib.error: - exit('\r' + _gc("lr") + - " ! Error: Couldn't decompress!\n + Data may not be compressed or may be encrypted.") + raise _CouldNotDecompressError("Couldn't decompress! Data may not be compressed or may be encrypted.") + # exit('\r' + _gc("lr") + + # " ! Error: Couldn't decompress!\n + Data may not be compressed or may be encrypted.") return data # Adds a numeric identifier -def add_num(name: str): +def add_num(name: str) -> str: + """ + Adds a numeric identifier to the input name. + + :param name: str: The name that needs an identifier. + :return: str: The name with a new identifier. + """ + + # Checks whether the input is valid + if not isinstance(name, str): + raise TypeError('`name` must be an instance of str.') + n = '' prefix = '' extension = '' diff --git a/InvisibleCharm/lib/operations/Embed.py b/InvisibleCharm/lib/operations/Embed.py index 26aed19..8b97883 100644 --- a/InvisibleCharm/lib/operations/Embed.py +++ b/InvisibleCharm/lib/operations/Embed.py @@ -7,6 +7,7 @@ import magic as _magic # We use zipfile library to embed files import zipfile as _zipfile +from typing import Union as _Union from log21 import get_colors as _gc from InvisibleCharm.lib.data.Prepare import add_num as _add_num from InvisibleCharm.Settings import embed_capable as _embed_capable @@ -14,19 +15,50 @@ from InvisibleCharm.lib.File import open_file as _open_file, save_file as _save_file, \ delete_source_file as _delete_source_file from InvisibleCharm.lib.data.Prepare import prepare_data as _prepare_data +from InvisibleCharm.lib.Exceptions import InvalidCoverDataTypeError as _InvalidCoverDataTypeError, \ + CoverDataTypeNotFoundError as _CoverDataTypeNotFoundError, NoEmbeddedFileFoundError as _NoEmbeddedFileFoundError, \ + NoEmbeddedDataFoundError as _NoEmbeddedDataFoundError __all__ = ['embed_file', 'extract_file', 'embed', 'extract'] # Embeds a file in a cover -def embed_file(source: str, cover: str, dest: str, delete_source: bool, compress: bool, encrypt_pass=None) -> None: +def embed_file(source: str, cover: str, dest: str, delete_source: bool, compress: bool, + encrypt_pass: _Union[str, bytes] = '', force_use_cover: bool = False) -> None: + """ + Embeds a file in a cover + + :param source: str: Source file path. + :param cover: str: Cover file path. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after embed process? + :param compress: bool: Do you want to compress the source data? + :param encrypt_pass: Union[str, bytes] = '': A password for encrypting the file + :param force_use_cover: bool = False: Use this cover data without raising any exceptions. + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(source, str): + raise TypeError('`source` must be an instance of str.') + if not _os.path.exists(source): + raise FileNotFoundError('`source` must be an existing file path.') + if not isinstance(cover, str): + raise TypeError('`cover` must be an instance of str.') + if not _os.path.exists(cover): + raise FileNotFoundError('`cover` must be an existing file path.') + if not isinstance(dest, str): + raise TypeError('`dest` must be an instance of str.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + # Reads and prepares source data source_data = _open_file(source, 'source', True, compress, encrypt_pass) # Reads cover file content cover_data = _open_file(cover, 'cover') - data = embed(source_data, cover_data, False) + data = embed(source_data, cover_data, False, force_use_cover=force_use_cover) # Saves the prepared data in the destination path _save_file(dest, data) @@ -37,29 +69,48 @@ def embed_file(source: str, cover: str, dest: str, delete_source: bool, compress # Embeds data in a cover -def embed(source_data: bytes, cover: bytes, compress: bool, encrypt_pass=None) -> bytes: +def embed(source_data: _Union[str, bytes], cover: _Union[str, bytes], compress: bool, + encrypt_pass: _Union[str, bytes] = '', force_use_cover: bool = False) -> bytes: + """ + Embeds data in a cover + + :param source_data: Union[str, bytes]: Data to embed. + :param cover: bytes: Cover data. + :param compress: bool: Do you want to compress the source data? + :param encrypt_pass: Union[str, bytes] = '': A password for encrypting the file + :param force_use_cover: bool = False: Use this cover data without raising any exceptions. + :return: bytes: Embedded data + """ + + # Checks whether the inputs are valid + if not isinstance(source_data, (str, bytes)): + raise TypeError('`data` must be an instance of str or bytes.') + if isinstance(source_data, str): + source_data = source_data.encode() + if not isinstance(cover, (str, bytes)): + raise TypeError('`data` must be an instance of str or bytes.') + if isinstance(cover, str): + cover = cover.encode() + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + # Prepares the data source_data = _prepare_data(source_data, True, compress, encrypt_pass) - _logger.debug(_gc("ly") + ' * Checking cover file type...', end='') - # Checks cover file type - valid = False - try: - cover_type = _magic.from_buffer(cover) - for t in _embed_capable: - if t.lower() in cover_type.lower(): - valid = True - except _magic.magic.MagicException: - confirm = input(_gc("lr") + f" ! Error: Couldn't identify file type!\n" + _gc("ly") + - ' * Do you still want to use this cover file?' + _gc("lw") + '(' + _gc("ly") + - f'Enter {_gc("lm")}Y{_gc("ly")} to confirm{_gc("lw")}){_gc("lr")}: ' - + _gc("lg")).lower() - if confirm == 'y': - valid = True - else: - exit() - if not valid: - exit('\r' + _gc("lr") + " ! Error: Cover File Type(" + cover_type + ") is not supported!") - _logger.debug('\r' + _gc("lg") + ' = Cover file is valid.') + + if not force_use_cover: + _logger.debug(_gc("ly") + ' * Checking cover file type...', end='') + # Checks cover file type + valid = False + try: + cover_type = _magic.from_buffer(cover) + for t in _embed_capable: + if t.lower() in cover_type.lower(): + valid = True + except _magic.magic.MagicException: + raise _CoverDataTypeNotFoundError("Couldn't identify cover data type!") + if not valid: + raise _InvalidCoverDataTypeError(f'Cover Data Type({cover_type}) is not supported!') + _logger.debug('\r' + _gc("lg") + ' = Cover file is valid.') data = cover _logger.debug(_gc("ly") + ' * Making an archive...', end='') @@ -91,10 +142,35 @@ def embed(source_data: bytes, cover: bytes, compress: bool, encrypt_pass=None) - # Extracts a file from an embedded file -def extract_file(source: str, dest: str, delete_source: bool, compress: bool, encrypt_pass=None) -> None: +def extract_file(source: str, dest: str, delete_source: bool, compress: bool, + encrypt_pass: _Union[str, bytes] = '') -> None: + """ + Embeds a file in a cover + + :param source: str: Source file path to decrypt. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after embed process? + :param compress: bool: Do you want to decompress the source data? + :param encrypt_pass: Union[str, bytes] = '': A password for decrypting the file + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(source, str): + raise TypeError('`source` must be an instance of str.') + if not _os.path.exists(source): + raise FileNotFoundError('`source` must be an existing file path.') + if not isinstance(dest, str): + raise TypeError('`dest` must be an instance of str.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + _logger.debug(_gc("ly") + ' * Opening file...', end='') # Opens the source file as a zip archive - archive = _zipfile.ZipFile(source) + try: + archive = _zipfile.ZipFile(source) + except _zipfile.BadZipfile: + raise _NoEmbeddedFileFoundError(f'No embedded file found in {source}') # Reads the hidden data from the source file with archive.open(archive.filelist[0], 'r') as source_file: data = source_file.read() @@ -114,7 +190,24 @@ def extract_file(source: str, dest: str, delete_source: bool, compress: bool, en # Extracts data from an embedded data -def extract(source_data: bytes, compress: bool, encrypt_pass=None) -> bytes: +def extract(source_data: _Union[str, bytes], compress: bool, encrypt_pass: _Union[str, bytes] = '') -> bytes: + """ + Extracts data from an embedded data + + :param source_data: : Union[str, bytes]: Data to extract. + :param compress: bool: Do you want to decompress the source data? + :param encrypt_pass: Union[str, bytes] = '': A password for decrypting the file + :return: bytes: Extracted data + """ + + # Checks whether the inputs are valid + if not isinstance(source_data, (str, bytes)): + raise TypeError('`data` must be an instance of str or bytes.') + if isinstance(source_data, str): + source_data = source_data.encode() + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + _logger.debug(_gc("ly") + ' * Opening file...', end='') # Generates a name for a temporarily zip archive archive_tmp_path = f'tmp' @@ -124,7 +217,10 @@ def extract(source_data: bytes, compress: bool, encrypt_pass=None) -> bytes: file.write(source_data) del source_data # Opens the source file as a zip archive - archive = _zipfile.ZipFile(archive_tmp_path) + try: + archive = _zipfile.ZipFile(archive_tmp_path) + except _zipfile.BadZipfile: + raise _NoEmbeddedDataFoundError('No embedded data found in the input data!') # Reads the hidden data from the source file with archive.open(archive.filelist[0], 'r') as source_file: data = source_file.read() diff --git a/InvisibleCharm/lib/operations/Image.pyx b/InvisibleCharm/lib/operations/Image.pyx index 076ac1c..3982ffa 100644 --- a/InvisibleCharm/lib/operations/Image.pyx +++ b/InvisibleCharm/lib/operations/Image.pyx @@ -26,6 +26,13 @@ cdef int vm(int n): # Prepares data and calculate suitable width and height for image cdef tuple calculate_size(bytes data, int mode): + """ + Prepares data and calculate suitable width and height for image. + + :param data: bytes + :param mode: int + :return: Tuple[bytes, int, int] + """ data += b'\x21' cdef int length, width, height, tmp1, tmp2 while True: @@ -50,11 +57,22 @@ cdef tuple calculate_size(bytes data, int mode): return data, width, height # Convert a file to an image -cpdef void to_image_file(str source, str dest, delete_source, compress, str encrypt_pass=None, int mode=3): +cpdef void to_image_file(str source, str dest, delete_source, compress, str encrypt_pass='', int mode=3): + """ + Convert a file to an image + + :param source: str: Source file path. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after the process? + :param compress: bool: Do you want to compress the source data? + :param encrypt_pass: str = '': A password for encrypting the file + :param mode: int = 3: Image mode. 3:RGB, 4:ARGB + :return: None + """ # Reads and prepares the source file data cdef bytes data = _open_file(source, 'source', True, compress, encrypt_pass) - image = to_image(data, False) + image = to_image(data, False, mode=mode) # Saves image in the destination path image.save(dest, format='png') @@ -65,7 +83,16 @@ cpdef void to_image_file(str source, str dest, delete_source, compress, str encr _delete_source_file(source) # Convert data to an image -cpdef to_image(bytes data, compress, str encrypt_pass=None, int mode=3): +cpdef to_image(bytes data, compress, str encrypt_pass='', int mode=3): + """ + Convert data to an image + + :param data: bytes: Data to convert to an image + :param compress: bool: Do you want to compress the data? + :param encrypt_pass: str = '': A password for encrypting the file + :param mode: int = 3: Image mode. 3:RGB, 4:ARGB + :return: PIL.Image.Image + """ # Reads and prepares the source file data data = _prepare_data(data, True, compress, encrypt_pass) @@ -100,6 +127,12 @@ cpdef to_image(bytes data, compress, str encrypt_pass=None, int mode=3): # Reads pixels and returns data cpdef bytes read_pixels(image: _Image.Image): + """ + Reads pixels and returns data + + :param image: PIL.Image.Image + :return: bytes: Extracted data + """ _logger.debug(_gc("ly") + ' * Loading pixels...', end='') # Loads image pixel map pixel_map = image.load() @@ -146,6 +179,16 @@ cpdef bytes read_pixels(image: _Image.Image): # Extract a file from an image pixels cpdef void from_image_file(str source, str dest, delete_source, compress, str encrypt_pass=None): + """ + Convert a file to an image + + :param source: str: Source image file path. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after the process? + :param compress: bool: Do you want to decompress the source data? + :param encrypt_pass: str = '': A password for decrypting the file + :return: None + """ # Reads the image file _logger.debug(_gc("ly") + ' * Opening image...', end='') image = _Image.open(source) @@ -166,6 +209,14 @@ cpdef void from_image_file(str source, str dest, delete_source, compress, str en # Extract data from an image pixels cpdef bytes from_image(image: _Image.Image, compress, str encrypt_pass=None): + """ + Convert data to an image + + :param image: PIL.Image.Image + :param compress: bool: Do you want to decompress the data? + :param encrypt_pass: str = '': A password for decrypting the file + :return: bytes + """ # Checks input types if not isinstance(image, _Image.Image): raise TypeError('`image` must be an instance of PIL.Image.Image class!') diff --git a/InvisibleCharm/lib/operations/Windows.py b/InvisibleCharm/lib/operations/Windows.py index 83c2b75..175b2a6 100644 --- a/InvisibleCharm/lib/operations/Windows.py +++ b/InvisibleCharm/lib/operations/Windows.py @@ -3,18 +3,48 @@ import os as _os +from typing import Union as _Union from log21 import get_colors as _gc from InvisibleCharm.lib.Console import logger as _logger, input, exit from InvisibleCharm.lib.data.Prepare import add_num as _add_num from InvisibleCharm.lib.File import get_names as _get_names, open_file as _open_file, save_file as _save_file, \ delete_source_file as _delete_source_file +from InvisibleCharm.lib.Exceptions import WinEmbeddedFileNotFoundError as _WinEmbeddedFileFoundError, \ + NoWinEmbeddedFileFoundError as _NoWinEmbeddedFileFoundError __all__ = ['win_embed', 'win_extract', 'win_attrib_hide', 'win_attrib_reveal'] # Hides a file in another Windows file def win_embed(source: str, dest: str, delete_source: bool, compress: bool, cover: str = None, - encrypt_pass=None) -> None: + encrypt_pass: _Union[str, bytes] = '') -> None: + """ + Hides a file in another Windows file. + + :param source: str: Source file path. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after embed process? + :param compress: bool: Do you want to compress the source data? + :param cover: str: Cover file path. + :param encrypt_pass: Union[str, bytes] = '': A password for encrypting the file + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(source, str): + raise TypeError('`source` must be an instance of str.') + if not _os.path.exists(source): + raise FileNotFoundError('`source` must be an existing file path.') + if not isinstance(dest, str): + raise TypeError('`dest` must be an instance of str.') + if cover: + if not isinstance(cover, str): + raise TypeError('`cover` must be an instance of str.') + if not _os.path.exists(cover): + raise FileNotFoundError('`cover` must be an existing file path.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + # Gets the list of available embedded names in the destination path names = _get_names(dest) # Generates a default name @@ -42,11 +72,12 @@ def win_embed(source: str, dest: str, delete_source: bool, compress: bool, cover name = default_name _logger.debug(_gc("ly") + ' * Preparing path...', end='') - if cover: + if cover and cover != dest: # Writes the cover data in the destination path with open(cover, 'rb') as cover_file: - with open(dest, 'wb') as dest_file: - dest_file.write(cover_file.read()) + cover = cover_file.read() + with open(dest, 'wb') as dest_file: + dest_file.write(cover) elif not _os.path.exists(dest): # Creates an empty file in the destination path with open(dest, 'w') as dest_file: @@ -65,22 +96,41 @@ def win_embed(source: str, dest: str, delete_source: bool, compress: bool, cover # Extracts a hidden file from a file in windows -def win_extract(source: str, dest: str, delete_source: bool, compress: bool, encrypt_pass=None) -> None: +def win_extract(source: str, dest: str, delete_source: bool, compress: bool, name: str = '', + encrypt_pass: _Union[str, bytes] = '') -> None: + """ + Extracts a hidden file from a file in Windows. + + :param source: str: Source file path. + :param dest: str: Destination file path. + :param delete_source: bool: Do you want to delete the source file after embed process? + :param compress: bool: Do you want to decompress the source data? + :param name: str = '': The name of the embedded file to extract(Automatically finds if the + :param encrypt_pass: Union[str, bytes] = '': A password for decrypting the file + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(source, str): + raise TypeError('`source` must be an instance of str.') + if not _os.path.exists(source): + raise FileNotFoundError('`source` must be an existing file path.') + if not isinstance(dest, str): + raise TypeError('`dest` must be an instance of str.') + if not isinstance(encrypt_pass, (str, bytes)) and encrypt_pass: + raise TypeError('`encrypt_pass` must be an instance of str or bytes.') + # Gets the list of available embedded names in destination path possible_names = _get_names(source) - name = '' if len(possible_names) == 0: - # Exits if no embedded file is available - exit(_gc("lr") + ' ! Error: No embedded file found!') + # Raises an exception if no embedded file is available + raise _NoWinEmbeddedFileFoundError("No win-embedded file found!") elif len(possible_names) == 1: # Choose the only embedded file automatically name = possible_names[0] - else: - # Asks user to choose one of the embedded files - while name not in possible_names: - _logger.info(_gc("lb") + 'Available names' + _gc("lr") + ': ' + _gc("lg") + - f'{_gc("lr")}, {_gc("lg")}'.join(possible_names)) - name = input(_gc("ly") + f'Enter the name of embedded file' + _gc("lr") + ': ' + _gc("lg")) + + if name not in possible_names: + raise _WinEmbeddedFileNotFoundError(f"There is no win-embedded file in '{source}' with name: '{name}'") # Reads and prepares data data = _open_file(source + ':' + name, 'source', False, compress, encrypt_pass) @@ -95,6 +145,19 @@ def win_extract(source: str, dest: str, delete_source: bool, compress: bool, enc # Changes a file windows attributes not to be shown in Windows explorer def win_attrib_hide(path: str) -> None: + """ + Changes a file windows attributes not to be shown in Windows explorer + + :param path: str: File path. + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not _os.path.exists(path): + raise FileNotFoundError('`path` must be an existing file path.') + _logger.info(_gc("ly") + ' * Running `attrib` command...', end='') # Runs Windows attrib command # +h : Adds Hidden File attribute @@ -106,6 +169,19 @@ def win_attrib_hide(path: str) -> None: # Changes a file windows attributes to be shown in Windows explorer def win_attrib_reveal(path: str) -> None: + """ + Changes a file windows attributes to be shown in Windows explorer + + :param path: str: File path. + :return: None + """ + + # Checks whether the inputs are valid + if not isinstance(path, str): + raise TypeError('`path` must be an instance of str.') + if not _os.path.exists(path): + raise FileNotFoundError('`path` must be an existing file path.') + _logger.info(_gc("ly") + ' * Running `attrib` command...', end='') # Runs Windows attrib command # -h : Removes Hidden File attribute diff --git a/MANIFEST.in b/MANIFEST.in index 5f9b1db..d7f0ddc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include InvisibleCharm/lib/operations/image.pyx \ No newline at end of file +include InvisibleCharm/lib/operations/Image.pyx \ No newline at end of file diff --git a/README.md b/README.md index 504f64c..c3ac1c9 100644 --- a/README.md +++ b/README.md @@ -13,34 +13,10 @@ Requirements - [pycryptodome](https://pycryptodome.readthedocs.io/en/latest/src/installation.html) - [Pillow](https://pillow.readthedocs.io/en/latest/installation.html) - [python-magic](https://pypi.org/project/python-magic/) +- [importlib_resources](https://pypi.org/project/importlib-resources/) *Note: You might need to install some of the requirements manually.* -Changes -------- - -### 2.2.0 - -`to_image` and `from_image` renamed to `to_image_file` and `from_image_file`. New `to_image` and `from_image` functions -use bytes and PIL.Image as input and output. - -`embed` and `extract` renamed to `embed_file` and `extract_file`. New `to_image` and `from_image` functions -use bytes as input and output. - -### 2.1.4 - -`MANIFEST.in` added. - -### 2.1.1 - 2.1.3 - -Auto release fixed. - -### 2.1.0 - -Using `Cython`, increased the speed of converting a file to an image and extracting the file from the image. - - - Install InvisibleCharm ---------------------- @@ -52,6 +28,11 @@ python -m pip install InvisibleCharm Or you can clone [the repository](https://github.com/MPCodeWriter21/InvisibleCharm) and run: +```commandline +git clone https://github.com/MPCodeWriter21/InvisibleCharm +cd InvisibleCharm +``` + ```commandline python setup.py install ``` @@ -88,6 +69,33 @@ optional arguments: --quiet, -q ``` +Changes +------- + +### 2.3.0 + +Some exceptions handled and some comments added to the files. + +### 2.2.0 + +`to_image` and `from_image` renamed to `to_image_file` and `from_image_file`. New `to_image` and `from_image` functions +use bytes and PIL.Image as input and output. + +`embed` and `extract` renamed to `embed_file` and `extract_file`. New `to_image` and `from_image` functions use bytes as +input and output. + +### 2.1.4 + +`MANIFEST.in` added. + +### 2.1.1 - 2.1.3 + +Auto release fixed. + +### 2.1.0 + +Using `Cython`, increased the speed of converting a file to an image and extracting the file from the image. + Examples -------- diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8862309..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -setuptools -Cython -python-magic -pycryptodome -Pillow -log21 \ No newline at end of file diff --git a/setup.py b/setup.py index 32fdd44..c55f7cd 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,26 @@ #!/usr/bin/env python3 # setup.py import os -from setuptools import setup, find_packages -from Cython.Build import cythonize +from setuptools import setup, find_packages, Extension +from distutils.command.build import build + + +class Build(build): + def finalize_options(self): + super().finalize_options() + from Cython.Build import cythonize + self.distribution.ext_modules = cythonize(self.distribution.ext_modules) + with open('README.md', 'r') as file: - long_description = file.read() + LONG_DESCRIPTION = file.read() with open('LICENSE.txt', 'r') as file: LICENSE = file.read() DESCRIPTION = 'InvisibleCharm is a python script that allows you to hide your files.' -VERSION = '2.2.0' -REQUIREMENTS = ['log21', 'Pillow', 'pycryptodome'] +VERSION = '2.3.0' +REQUIREMENTS = ['log21', 'Pillow', 'pycryptodome', 'importlib_resources'] if os.name == 'nt': REQUIREMENTS.append('python-magic-bin') else: @@ -26,8 +34,9 @@ author_email='', license=LICENSE, description=DESCRIPTION, - long_description=long_description, + long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', + setup_requires=["cython"], install_requires=REQUIREMENTS, packages=find_packages(), entry_points={ @@ -35,14 +44,15 @@ 'InvisibleCharm = InvisibleCharm:entry_point' ] }, - ext_modules=cythonize('InvisibleCharm/lib/operations/Image.pyx'), + ext_modules=[Extension(name='InvisibleCharm.lib.operations.Image', + sources=['InvisibleCharm/lib/operations/Image.pyx'])], keywords=['python', 'python3', 'CodeWriter21', 'Hide', 'Hidden', 'InvisibleCharm', 'Invisible', 'Charm'], classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Operating System :: Unix", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows" ], - include_package_data=True + include_package_data=True, + cmdclass={"build": Build} )