Unbin - Improved

When I first wrote Unbin (around 2007), the thing was a mess.

I'd been using Linux for about a year and (for me) bash scripting was all the rage. The original Unbin was made of 8 or 9 different bash scripts, 2 perl scripts, and then all the other stuff that goes in a standard Debian package (a control file, a post-install, etc). Not to mention it included 6 different scripts for 'easter eggs.' The dependencies were crazy for such a simple program.

As proud as I was of my little victory, I promised myself one day I'd make it better.

I've ditched the gazillion bash scripts, and the two perl scripts (I don't hate perl, don't get me wrong... but the thing has issues...) and replaced it with a single Python script. I feel like Python is (for me) all the rage these days... I've also ditched zenity in favor of Tkinter.


I've kept the ability to reverse text, to convert to and from binary and hexadecimal, and added functionality to encrypt/decrypt a 'translated' file using GPG.

Key Features:
  • Binary Conversion: Convert text to 8-bit group binary and back.
  • Hexadecimal Conversion: Convert text to 2-character group hexadecimal and back.
  • Reverse Text: Quick reverse strings.
  • GPG Support: Encrypt and decrypt files during conversion (GUI only).
  • GUI & CLI: Automatically fall back to CLI if GUI is not available.


Program Start:



"Hello" in binary:



"Hello" in hexadecimal:



Code:


Project page on GitHub

#Unbin v3.0
#by Jonathan Torres

import sys
import os
import binascii
import argparse
import subprocess

# Version and Metadata
VERSION = "3.0 (Python Refactor)"
LICENSE = """Unbin is free software distributed under the terms of the GNU General Public License, version 3,
or any later version published by the Free Software Foundation.
You have the freedom to use, study, modify, and redistribute this software.
Any derivative work must also be under GPLv3.
See /usr/share/Unbin/docs/gpl-3.0.txt for full details."""

# Core Logic Functions

def text_to_binary(text):
    return ' '.join(format(ord(c), '08b') for c in text)

def binary_to_text(binary):
    binary = binary.strip().replace(' ', '').replace('\n', '').replace('\r', '')
    if not binary: return ""
    try:
        # Handle cases where multiple bytes are present
        n = int(binary, 2)
        return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode('utf-8')
    except Exception as e:
        return f"Error: {e}"

def text_to_hex(text):
    h = binascii.hexlify(text.encode('utf-8')).decode('utf-8')
    return ' '.join(h[i:i+2] for i in range(0, len(h), 2))

def hex_to_text(hex_str):
    hex_str = hex_str.strip().replace(' ', '').replace('\n', '').replace('\r', '')
    if not hex_str: return ""
    try:
        return binascii.unhexlify(hex_str).decode('utf-8')
    except Exception as e:
        return f"Error: {e}"

def reverse_text(text):
    return text[::-1]

# File Operations (CLI)

def read_file_cli(path):
    try:
        if path.endswith(".gpg"):
             print(f"Decryption of {path} requires GUI or manual gpg command in CLI mode for now.")
             return None
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            return f.read()
    except Exception as e:
        print(f"Error reading file: {e}")
        return None

# GUI implementation

class UnbinGUI:
    def __init__(self, root, tk, messagebox, filedialog, simpledialog):
        self.root = root
        self.tk = tk
        self.messagebox = messagebox
        self.filedialog = filedialog
        self.simpledialog = simpledialog
        
        self.root.title(f"Unbin v{VERSION}")
        self.root.geometry("400x300")
        
        tk.Label(root, text="Choose encoding type:").pack(pady=10)
        
        self.mode = tk.StringVar(value="Binary")
        modes = ["Binary", "Hexadecimal", "Reverse"]
        for m in modes:
            tk.Radiobutton(root, text=m, variable=self.mode, value=m).pack()

        tk.Button(root, text="Process Text", command=self.process_text_dialog).pack(pady=10)
        tk.Button(root, text="Process File", command=self.process_file_dialog).pack(pady=5)
        tk.Button(root, text="License", command=lambda: messagebox.showinfo("License", LICENSE)).pack(pady=5)

    def process_text_dialog(self):
        msg = self.simpledialog.askstring("Input", f"Enter message to {self.mode.get()}:")
        if msg is None: return
        
        mode = self.mode.get()
        if mode == "Binary":
            action = self.messagebox.askquestion("Action", "Convert TO binary? (No = FROM binary)")
            res = text_to_binary(msg) if action == 'yes' else binary_to_text(msg)
        elif mode == "Hexadecimal":
            action = self.messagebox.askquestion("Action", "Convert TO hex? (No = FROM hex)")
            res = text_to_hex(msg) if action == 'yes' else hex_to_text(msg)
        else:
            res = reverse_text(msg)
            
        self.show_result(res)

    def process_file_dialog(self):
        path = self.filedialog.askopenfilename()
        if not path: return
        
        content = self.gui_read_file(path)
        if content is None: return
        
        mode = self.mode.get()
        if mode == "Binary":
            action = self.messagebox.askquestion("Action", "Convert TO binary? (No = FROM binary)")
            res = text_to_binary(content) if action == 'yes' else binary_to_text(content)
        elif mode == "Hexadecimal":
            action = self.messagebox.askquestion("Action", "Convert TO hex? (No = FROM hex)")
            res = text_to_hex(content) if action == 'yes' else hex_to_text(content)
        else:
            res = reverse_text(content)
            
        if self.messagebox.askyesno("Save", "Save result to file?"):
            save_path = self.filedialog.asksaveasfilename()
            if save_path:
                encrypt = self.messagebox.askyesno("Encrypt", "Encrypt with GPG?")
                self.gui_save_file(res, save_path, encrypt)
        else:
            self.show_result(res)

    def gui_read_file(self, path):
        try:
            if path.endswith(".gpg"):
                passphrase = self.simpledialog.askstring("GPG", "Enter passphrase:", show='*')
                if not passphrase: return None
                cmd = ["gpg", "--batch", "--passphrase", passphrase, "-d", path]
                res = subprocess.run(cmd, capture_output=True, text=True)
                if res.returncode != 0:
                    self.messagebox.showerror("Error", f"GPG Decryption failed: {res.stderr}")
                    return None
                return res.stdout
            else:
                with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                    return f.read()
        except Exception as e:
            self.messagebox.showerror("Error", f"Could not read file: {e}")
            return None

    def gui_save_file(self, content, path, encrypt=False):
        try:
            temp_path = path
            if encrypt:
                temp_path = path + ".tmp"
            
            with open(temp_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            if encrypt:
                passphrase = self.simpledialog.askstring("GPG", "Enter new passphrase:", show='*')
                if not passphrase:
                    os.remove(temp_path)
                    return False
                cmd = ["gpg", "--batch", "--passphrase", passphrase, "-c", temp_path]
                res = subprocess.run(cmd, capture_output=True)
                os.remove(temp_path)
                if res.returncode != 0:
                    self.messagebox.showerror("Error", "GPG Encryption failed.")
                    return False
                final_path = temp_path + ".gpg"
                if os.path.exists(path + ".gpg"):
                     os.replace(final_path, path + ".gpg")
                else:
                     os.rename(final_path, path + ".gpg")
            return True
        except Exception as e:
            self.messagebox.showerror("Error", f"Could not save file: {e}")
            return False

    def show_result(self, result):
        top = self.tk.Toplevel(self.root)
        top.title("Result")
        text_widget = self.tk.Text(top, wrap='word')
        text_widget.insert('1.0', result)
        text_widget.pack(expand=True, fill='both')
        self.tk.Button(top, text="Close", command=top.destroy).pack()

# CLI Mode

def cli_mode(args):
    if args.l:
        print(LICENSE)
        return
    
    input_data = ""
    if args.f:
        input_data = read_file_cli(args.f)
        if input_data is None: return
    elif args.text:
        input_data = " ".join(args.text)
    else:
        # Interactive CLI
        print("Choose encoding: (b)in, (h)ex, (r)ev")
        choice = input("> ").strip().lower()
        if choice not in ['b', 'h', 'r']:
             print("Invalid choice")
             return
        print("Enter message:")
        input_data = input("> ")
        if choice == 'b': args.b = True
        elif choice == 'h': args.h = True
        elif choice == 'r': args.r = True

    result = ""
    if args.b: result = text_to_binary(input_data)
    elif args.bt: result = binary_to_text(input_data)
    elif args.h: result = text_to_hex(input_data)
    elif args.ht: result = hex_to_text(input_data)
    elif args.r or args.rt: result = reverse_text(input_data)
    
    print(result)

# Main

def main():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-b", action="store_true")
    parser.add_argument("-bt", action="store_true")
    parser.add_argument("-h", action="store_true")
    parser.add_argument("-ht", action="store_true")
    parser.add_argument("-r", action="store_true")
    parser.add_argument("-rt", action="store_true")
    parser.add_argument("-f", type=str)
    parser.add_argument("-l", action="store_true")
    parser.add_argument("-t", "--terminal", action="store_true")
    parser.add_argument("-nogui", action="store_true")
    parser.add_argument("-help", "--help", action="store_true")
    parser.add_argument("-moo", action="store_true")
    parser.add_argument("text", nargs='*', default=None)

    args, unknown = parser.parse_known_args()

    if args.help:
        print("Unbin v3.0 Usage:")
        print("  Unbin [-b|-bt|-h|-ht|-r|-rt] [-f file] [text]")
        print("  -t / -nogui : Terminal mode")
        print("  -l          : Show license")
        return

    if args.moo:
        print("There's no easter in these eggs... @~@")
        return

    use_terminal = args.terminal or args.nogui or args.b or args.bt or args.h or args.ht or args.r or args.rt or args.f or args.l or (args.text and len(args.text) > 0)

    if use_terminal:
        cli_mode(args)
    else:
        try:
            import tkinter as tk
            from tkinter import messagebox, filedialog, simpledialog
            root = tk.Tk()
            app = UnbinGUI(root, tk, messagebox, filedialog, simpledialog)
            root.mainloop()
        except ImportError:
            print("Tkinter not found. Falling back to terminal mode.")
            cli_mode(args)
        except Exception as e:
            print(f"Could not start GUI: {e}")
            print("Falling back to terminal mode.")
            cli_mode(args)

if __name__ == "__main__":
    main()