master 9b8503a3ad34 cached
37 files
53.0 MB
189.8k tokens
469 symbols
1 requests
Download .txt
Showing preview only (809K chars total). Download the full file or copy to clipboard to get everything.
Repository: Gadzhovski/TRACE-Forensic-Toolkit
Branch: master
Commit: 9b8503a3ad34
Files: 37
Total size: 53.0 MB

Directory structure:
gitextract_yykb5mfu/

├── .gitignore
├── LICENSE
├── README.md
├── install_macos_linux_WSL.sh
├── main.py
├── modules/
│   ├── about.py
│   ├── converter.py
│   ├── exif_tab.py
│   ├── file_carving.py
│   ├── hex_tab.py
│   ├── mainwindow.py
│   ├── metadata_tab.py
│   ├── registry.py
│   ├── text_tab.py
│   ├── unified_application_manager.py
│   ├── verification.py
│   ├── veriphone_api.py
│   └── virus_total_tab.py
├── requirements.txt
├── requirements_macos_silicon.txt
├── styles/
│   ├── dark_theme.qss
│   └── light_theme.qss
└── tools/
    ├── Arsenal-Image-Mounter-v3.10.257/
    │   ├── Arsenal Recon - End User License Agreement.txt
    │   ├── ArsenalImageMounter.deps.json
    │   ├── ArsenalImageMounter.log
    │   ├── ArsenalImageMounter.runtimeconfig.json
    │   ├── aim_cli.runtimeconfig.json
    │   ├── readme.txt
    │   └── readme_cli.txt
    └── sleuthkit-4.12.1-win32/
        ├── NEWS.txt
        ├── README-win32.txt
        ├── README.txt
        ├── bin/
        │   └── mactime.pl
        ├── lib/
        │   ├── libtsk.lib
        │   └── libtsk_jni.lib
        └── licenses/
            ├── IBM-LICENSE
            └── cpl1.0.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Ignore E01, dd, and raw image files in the root directory
/*.E01
/*.dd
/*.raw

# Ignore IDE-specific folders and files
.idea/

# Ignore config files
/config.ini

# Ignore the carved_files directory
/carved_files/

# Ignore virtual environment folders
/venv/

# Ignore Python cache
__pycache__

# Ignore build artifacts
/build/
/dist/
*.spec
build_exe.py      

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Radoslav Gadzhovski

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.


================================================
FILE: README.md
================================================
<h1 align="center">Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)</h1>

<p align="center">
  TRACE is a digital forensic tool I developed as my final year project. It provides an intuitive interface for analyzing disk images and includes a range of functionalities to assist forensic examiners in extracting and viewing the contents of various image file formats.
</p>

<p align="center">
  <img src="Icons/logo_prev_ui.png" alt="TRACE Logo" width="400"/>
</p>

## Navigation 🧭 

- [Preview 👀](#preview-)
- [Features 🌟](#features-)
- [Screenshots 📸](#screenshots-)
- [Supported Image Formats 💾](#supported-image-formats-)
- [Tested File Systems 🗂️](#tested-file-systems-%EF%B8%8F)
- [Cross-Platform Compatibility 🖥️💻](#cross-platform-compatibility-%EF%B8%8F)
- [Getting Started 🚀](#getting-started-)
  - [Prerequisites 🛠️](#prerequisites-)
  - [Configuration ⚙️](#configuration-%EF%B8%8F)
  - [Running the Tool ▶️](#running-the-tool-%EF%B8%8F)
- [Built With 🧱](#built-with-)
- [Work in Progress 🛠️](#work-in-progress-)
- [Testing & Feedback 🧪](#testing--feedback-)
- [Contributing 🤝](#contributing-)
- [Socials 👨‍💻](#socials-)


## Preview 👀 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

<p>
  <br/>
  <img src="Icons/readme/Preview_Dark.png" alt="TRACE Preview" width="100%"/>
  <br/>
</p>

<br>

## Features 🌟 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

✅ ***Image Mounting**: Mount forensic disk images. (Windows only) \
✅ **Tree Viewer**: Navigate through the disk image structure, including partitions and files.\
✅ **Detailed File Analysis**: View file content in different formats, such as HEX, text, and application-specific views.\
✅ **EXIF Data Extraction**: Extract and display EXIF metadata from photos.\
✅ **Registry Viewer**: View and examine Windows registry files.\
✅ **Basic File Carving**: Recover deleted files from disk images.\
✅ **Virus Total API Integration**: Check files for malware using the Virus Total API.\
✅ **E01 Image Verification**: Verify the integrity of E01 disk images.\
✅ **Convert E01 to Raw**: Convert E01 disk images to raw format.\
✅ **Message Decoding**: Decode messages from base64, binary, and other encodings.

<br>

## Screenshots 📸 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

### Registry Browser 🗂️

<p>
  <br/>
  <img src="Icons/readme/registry.png" alt="Registry Browser" width="90%"/>
  <br/>
</p>


### File Carving 🔪

<p>
  <br/>
  <img src="Icons/readme/carving.png" alt="File Carving" width="90%"/>
  <br/>
</p>

### File Search 🔍
<p>
  <br/>
  <img src="Icons/readme/file_search.png" alt="Image Verification" width="80%"/>
  <br/>
</p>

### Image Verification ✅

<p>
  <br/>
  <img src="Icons/readme/trace_verify.png" alt="Image Verification" width="70%"/>
  <br/>
</p>

<br>



## Supported Image Formats 💾 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

| Image Format                                   | Extensions             | Split   |  Unsplit |
|------------------------------------------------|------------------------|---------|----------|
| EnCase® Image File (EVF / Expert Witness Format)| `*.E01` `*.Ex01`       | ✔️      | ✔️       |
| SMART/Expert Witness Image File                | `*.s01`                | ✔️      | ✔️       |
| Single Image Unix / Linux DD / Raw             | `*.dd`, `*.img`, `*.raw` | ✔️      | ✔️       |
| ISO Image File                                 | `*.iso`                |         | ✔️       |
| AccessData Image File                          | `*.ad1`                | ✔️       | ✔️        |

<br>

## Tested File Systems 🗂️ &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

| File System | Tested |
|-------------|--------|
| NTFS        | ✔️     |
| FAT32       |        |
| exFAT       |        |
| HFS+        |        |
| APFS        |        |
| EXT2,3,4    |        |

<br>


## Cross-Platform Compatibility 🍏🐧🗔  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

| Operating System                   | Screenshot                                                                                                           |
|------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **macOS Sonoma** 🍏                | <a href="Icons/readme/macos.png"><img src="Icons/readme/macos.png" alt="macOS Screenshot" width="900"/></a>          |
| **Kali Linux 2024** 🐧             | <a href="Icons/readme/kali.png"><img src="Icons/readme/kali.png" alt="Kali Linux Screenshot" width="900"/></a>       |
| **\*WSL2 - Ubuntu 22.04.3 LTS** 🐧 | <a href="Icons/readme/wsl3.png"><img src="Icons/readme/wsl3.png" alt="Kali Linux Screenshot" width="900"/></a>        |
| **Windows 10** 🗔                  | <a href="Icons/readme/windows10.png"><img src="Icons/readme/windows10.png" alt="Windows Screenshot" width="900"/></a> |



## Getting Started 🚀 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

### Installation ⚙️


#### **Windows:**
1.  Install Python 3.11<br>
    (⚠️ Python 3.12 is not supported)<br>
    [👉 Download from python.org](https://www.python.org/downloads/release/python-3110/)

2.  Install Microsoft C++ Build Tools<br>
    [👉 Download Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)

    During setup, ensure the following workloads are selected:

    - ✅ Desktop development with C++
    - ✅ C++ build tools

3.  Create and activate a virtual environment

    ```bash
    python -m venv venv
    venv\Scripts\activate
    ```

4.  Install dependencies

    ```bash
    pip install -r requirements.txt
    ```

5.  Run the tool

    ```bash
    python main.py
    ```



#### **macOS (Apple Silicon) & Linux (Ubuntu/WSL):**
1.  Make the installation script executable:

    ```bash
    chmod +x install_macos_linux_WSL.sh
    ```

2.  Run the installation script:

    ```bash
    ./install_macos_linux_WSL.sh
    ```

    The script will:
    - ✅ Create and activate a Python 3.11 virtual environment
    - ✅ Detect your system (macOS or Linux)
    - ✅ Install required system dependencies (via Homebrew or apt)
    - ✅Install the appropriate Python packages:
        * `requirements_macos_silicon.txt` → macOS
        * `requirements.txt` → Linux
    - ✅ After installation, it will automatically activate your virtual environment and notify you that it’s ready to use.

3.  Run the Tool

    Once the virtual environment is activated (you’ll see `(venv)` in your terminal prompt):

    ```bash
    python main.py
    ```


### Configuration ⚙️ 

**API Keys Configuration**:The tool integrates with VirusTotal and Veriphone APIs, and you will need to provide your own API keys to use these features. To update the API keys, go to the Options menu and select API Keys submenu.




## Built With 🧱  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

- [pytsk3](https://pypi.org/project/pytsk3/) - Python bindings for the SleuthKit
- [libewf-python](https://github.com/libyal/libewf) - Library to access the Expert Witness Compression Format (EWF)
- [PySide6](https://pypi.org/project/PySide6/) - Used for the GUI components.
- [Arsenal Image Mounter](https://arsenalrecon.com/products/image-mounter/) - For mounting forensic disk images.


## Work in Progress 🧑‍🔧  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

- **Cross-Platform Image Mounting**: Image mounting currently works only on Windows using the Arsenal Image Mounter executable. The aim is to make this feature work across all platforms without relying on external executables.
- **File Carving**: The verification of carved files needs improvement, as it may carve data fragments that are not actual files.
- **Color Issues in Dark Mode**: The software currently has some colour display issues on Linux and macOS systems when using dark mode. Certain UI elements may not be clearly visible or may appear incorrectly.


## Contributing 🤝 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)

I welcome contributions from the community to help improve TRACE! If you're interested in contributing, here’s how you can get involved:


1. **Report Issues**: If you find any bugs or have suggestions for improvements, please [open an issue](https://github.com/Gadzhovski/TRACE-Forensic-Toolkit/issues) on GitHub. Provide as much detail as possible to help address the issue effectively.
2. **Submit a Pull Request**: If you have a fix or feature you’d like to contribute, please [fork the repository](https://github.com/Gadzhovski/TRACE-Forensic-Toolkit/fork), make your changes, and submit a pull request. Ensure your code adheres to the coding standards and includes tests where applicable.


## Socials 👨‍💻 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)


[![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://linkedin.com/in/radoslav-gadzhovski)

<br>

![Version](https://img.shields.io/badge/version-1.2.0-purple.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)




================================================
FILE: install_macos_linux_WSL.sh
================================================
#!/bin/bash
set -e

# === COLORS ===
GREEN="\033[1;32m"
LIGHT_GREEN="\033[38;5;82m"
CYAN="\033[1;36m"
MAGENTA="\033[1;35m"
YELLOW="\033[1;33m"
R="\033[0m"

clear

# === Animated intro ===
animate_intro() {
    local frames=("⣾" "⣷" "⣯" "⣟" "⡿" "⢿" "⣻" "⣽")
    echo -ne "${MAGENTA}Launching TRACE Installer "
    for i in {1..20}; do
        printf "\b%s" "${frames[$((i % 8))]}"
        sleep 0.08
    done
    echo -e "${R}\n"
}

# === Pulsing TRACE logo (with aligned borders) ===
print_banner() {
    local colors=("\033[38;5;48m" "\033[38;5;118m" "\033[38;5;83m" "\033[38;5;77m")
    for i in {0..3}; do
        clear
        echo -e "${CYAN}┌────────────────────────────────────────────────────────────────────┐${R}"
        echo -e "${CYAN}│                                                                    │${R}"
        echo -e "${CYAN}│${R}           ${colors[$i]}████████╗██████╗  █████╗ ██████╗███████╗${R}                 ${CYAN}│${R}"
        echo -e "${CYAN}│${R}           ${colors[$i]}╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝${R}                ${CYAN}│${R}"
        echo -e "${CYAN}│${R}              ${colors[$i]}██║   ██████╔╝███████║██║     █████╗${R}                  ${CYAN}│${R}"
        echo -e "${CYAN}│${R}              ${colors[$i]}██║   ██╔══██╗██╔══██║██║     ██╔══╝${R}                  ${CYAN}│${R}"
        echo -e "${CYAN}│${R}              ${colors[$i]}██║   ██║  ██║██║  ██║╚██████╗███████╗${R}                ${CYAN}│${R}"
        echo -e "${CYAN}│${R}              ${colors[$i]}╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚══════╝${R}                ${CYAN}│${R}"
        echo -e "${CYAN}│                                                                    │${R}" 
        echo -e "${CYAN}│${R}                ${MAGENTA}TRACE Forensic Toolkit Installer${R}                    ${CYAN}│${R}"
        echo -e "${CYAN}│${R}                ${YELLOW}Compatible with macOS • Linux • WSL${R}                 ${CYAN}│${R}"
        echo -e "${CYAN}└────────────────────────────────────────────────────────────────────┘${R}\n"
        sleep 0.12
    done
}

# === Run intro and banner ===
animate_intro
print_banner

# === Detect OS type ===
OS_TYPE=$(uname)
if [[ "$OS_TYPE" == "Darwin" ]]; then
    DETECTED_OS="macOS"
elif [[ "$OS_TYPE" == "Linux" ]]; then
    if grep -qi "microsoft" /proc/version; then
        DETECTED_OS="WSL"
    else
        DETECTED_OS="Linux"
    fi
else
    echo -e "${RED}❌ Unsupported OS: $OS_TYPE${R}"
    echo "This installer supports macOS, Linux, and WSL only."
    exit 1
fi

echo -e "${CYAN}Detected operating system:${R} ${YELLOW}$DETECTED_OS${R}\n"

# === Confirm OS or override ===
read -p "Proceed with $DETECTED_OS installation? (y/n or type 'macos'/'linux'/'wsl' to override): " USER_INPUT
USER_INPUT=$(echo "$USER_INPUT" | tr '[:upper:]' '[:lower:]')

if [[ "$USER_INPUT" == "n" ]]; then
    echo -e "${RED}Installation cancelled.${R}"
    exit 0
elif [[ "$USER_INPUT" == "macos" ]]; then
    USER_OS="macOS"
elif [[ "$USER_INPUT" == "linux" ]]; then
    USER_OS="Linux"
elif [[ "$USER_INPUT" == "wsl" ]]; then
    USER_OS="WSL"
else
    USER_OS="$DETECTED_OS"
fi

echo -e "\n${MAGENTA}➡ Proceeding with installation for:${R} ${YELLOW}$USER_OS${R}"
echo "------------------------------------------------------------"
echo ""

# ------------------------------------------------------------------------------
# macOS INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "macOS" ]]; then
    echo -e "${CYAN}🍏 Installing macOS system dependencies...${R}"

    if ! command -v brew &> /dev/null; then
        echo -e "${YELLOW}Homebrew not found.${R}"
        read -p "Would you like to install Homebrew now? (y/n): " install_brew
        if [[ "$install_brew" == "y" || "$install_brew" == "Y" ]]; then
            /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
            echo -e "${GREEN}✅ Homebrew installed successfully.${R}"
        else
            echo -e "${RED}❌ Homebrew is required. Exiting.${R}"
            exit 1
        fi
    fi

    brew update
    brew install ffmpeg poppler libmagic

    echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
    python3 -m venv venv

    echo -e "\n${CYAN}📥 Installing Python packages...${R}"
    source venv/bin/activate
    pip install --upgrade pip setuptools wheel
    pip install --no-cache-dir -r requirements_macos_silicon.txt
    deactivate

    echo -e "\n${GREEN}✅ Installation complete!${R}"
    echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
    echo -e "   ${YELLOW}source venv/bin/activate${R}"
    echo -e "Then start the tool with:"
    echo -e "   ${YELLOW}python main.py${R}\n"
    echo "When finished, type 'deactivate'."
    exit 0
fi

# ------------------------------------------------------------------------------
# LINUX INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "Linux" ]]; then
    echo -e "${CYAN}🐧 Installing Linux system dependencies...${R}"
    sudo apt update
    sudo apt install -y python3 python3-venv python3-pip libxcb-cursor0 libva-dev libva-drm2 ewf-tools

    echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
    python3 -m venv venv

    echo -e "\n${CYAN}📥 Installing Python packages...${R}"
    source venv/bin/activate
    pip install --upgrade pip setuptools wheel
    pip install --no-cache-dir -r requirements_macos_silicon.txt
    deactivate

    echo -e "\n${GREEN}✅ Installation complete!${R}"
    echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
    echo -e "   ${YELLOW}source venv/bin/activate${R}"
    echo -e "Then start the tool with:"
    echo -e "   ${YELLOW}python main.py${R}\n"
    echo "When finished, type 'deactivate'."
    exit 0
fi

# ------------------------------------------------------------------------------
# WSL INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "WSL" ]]; then
    echo -e "${CYAN}🐧 Installing WSL (Ubuntu) dependencies...${R}"
    sudo apt update
    sudo apt install -y git python3-pip python3-venv libgl1-mesa-glx libxkbcommon0 libxkbcommon-x11-0 libegl1 \
                        libxcb-xinerama0 qt5dxcb-plugin qt5-wayland \
                        libqt5dbus5 libqt5widgets5 libqt5network5 libqt5gui5 libqt5core5a libqt5svg5 qtwayland5 \
                        nvidia-cuda-toolkit pulseaudio

    echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
    python3 -m venv venv

    echo -e "\n${CYAN}📥 Installing Python packages...${R}"
    source venv/bin/activate
    pip install --upgrade pip setuptools wheel
    pip install --no-cache-dir -r requirements_macos_silicon.txt
    deactivate

    echo -e "\n${GREEN}✅ Installation complete!${R}"
    echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
    echo -e "   ${YELLOW}source venv/bin/activate${R}"
    echo -e "Then start the tool with:"
    echo -e "   ${YELLOW}python main.py${R}\n"
    echo "When finished, type 'deactivate'."
    exit 0
fi

================================================
FILE: main.py
================================================

from PySide6.QtWidgets import QApplication
from modules.mainwindow import MainWindow


if __name__ == '__main__':
    app = QApplication([])

    window = MainWindow()
    window.show()
    app.exec()




================================================
FILE: modules/about.py
================================================
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QFont, QPalette, QColor
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout


class AboutDialog(QDialog):
    def __init__(self, parent=None):
        super(AboutDialog, self).__init__(parent)

        self.setWindowTitle("About Trace")
        layout = QVBoxLayout(self)

        # Load and scale the logo
        logo = QLabel(self)
        pixmap = QPixmap('Icons/logo.png')  # Ensure 'Icons/logo.png' is the correct path
        # Adjust the logo size here
        scaled_pixmap = pixmap.scaled(400, 400, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        logo.setPixmap(scaled_pixmap)
        logo.setAlignment(Qt.AlignCenter)  # Center the logo
        layout.addWidget(logo)

        # Software information
        title_label = QLabel("Trace - Toolkit for Retrieval and Analysis of Cyber Evidence")
        title_label.setAlignment(Qt.AlignCenter)
        title_label.setFont(QFont('Arial', 20, QFont.Bold))  # Set the font, size, and weight
        title_label.setPalette(QPalette(QColor('blue')))  # Set the text color
        layout.addWidget(title_label)

        version_label = QLabel("Version 1.0.0")
        version_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(version_label)

        author_label = QLabel("Author: Radoslav Gadzhovski")
        author_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(author_label)

        # Add a button to close the dialog
        button_layout = QHBoxLayout()
        button_layout.addStretch()  # Add stretchable space on the left
        close_button = QPushButton("Close")
        close_button.setFixedSize(100, 30)  # Set the size of the button
        close_button.clicked.connect(self.close)
        button_layout.addWidget(close_button)  # Add the button to the layout
        button_layout.addStretch()  # Add stretchable space on the right

        # Add the QHBoxLayout to the main QVBoxLayout
        layout.addLayout(button_layout)

        self.setLayout(layout)
        # Set the size of the dialog
        self.setFixedSize(500, 700)


================================================
FILE: modules/converter.py
================================================
import os
import subprocess

import pyewf
from PySide6.QtCore import Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
    QLabel, QFileDialog, QComboBox,
    QLineEdit, QHBoxLayout, QFormLayout
)
from PySide6.QtWidgets import (
    QMainWindow, QPushButton, QWidget, QRadioButton,
    QGroupBox, QVBoxLayout, QMessageBox, QStackedWidget
)


# Helper Function to List Drives (For Physical and Logical Drive Selection)
def list_drives():
    if os.name == "nt":
        # Using PowerShell command to list drives on Windows
        command = ["powershell", "-NoProfile", "Get-WmiObject Win32_DiskDrive | Select-Object Model, DeviceID"]
    elif os.name == "darwin":
        # Using diskutil to list drives on macOS
        command = ["diskutil", "list"]
    else:
        raise Exception("Unsupported OS")

    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode != 0:
        raise Exception("Failed to list drives")
    return result.stdout


class Main(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Convert E01 to DD/RAW")
        self.setGeometry(100, 100, 400, 400)
        # set logo
        self.setWindowIcon(QIcon('Icons/logo.png'))

        self.stacked_widget = QStackedWidget()
        self.setCentralWidget(self.stacked_widget)

        self.init_ui()

    def init_ui(self):
        self.select_source_dialog = SelectSourceDialog(self)
        self.stacked_widget.addWidget(self.select_source_dialog)

        self.conversion_widget = ConversionWidget(self)
        self.stacked_widget.addWidget(self.conversion_widget)

        self.drive_selection_widget = DriveSelectionWidget(self)
        self.stacked_widget.addWidget(self.drive_selection_widget)

        self.select_source_dialog.sourceSelected.connect(self.show_specific_widget)
        self.drive_selection_widget.backRequested.connect(self.show_select_source)

    def show_specific_widget(self, widget_name):
        if widget_name == "conversion":
            self.stacked_widget.setCurrentWidget(self.conversion_widget)
        elif widget_name == "folder_contents":
            # Handle folder contents selection
            pass  # Placeholder for actual implementation
        elif widget_name == "physical_drive":
            self.stacked_widget.setCurrentWidget(self.drive_selection_widget)

        elif widget_name == "logical_drive":
            # Handle logical drive selection
            pass  # Placeholder for actual implementation

    def show_select_source(self):
        self.stacked_widget.setCurrentWidget(self.select_source_dialog)


# New Widget for Drive Selection
class DriveSelectionWidget(QWidget):
    backRequested = Signal()
    driveSelected = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout(self)
        self.drive_combo = QComboBox()
        try:
            for drive in list_drives().split('\n'):
                if drive.strip():
                    self.drive_combo.addItem(drive.strip())
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to list drives: {e}")

        layout.addWidget(QLabel("Select Drive:"))
        layout.addWidget(self.drive_combo)

        select_button = QPushButton("Select")
        select_button.clicked.connect(self.on_select_clicked)
        layout.addWidget(select_button)

        back_button = QPushButton("Back")
        back_button.clicked.connect(lambda: self.backRequested.emit())
        layout.addWidget(back_button)

    def on_select_clicked(self):
        selected_drive = self.drive_combo.currentText()
        self.driveSelected.emit(selected_drive.split()[-1])  # Assuming the device ID is the last part


class SelectSourceDialog(QWidget):
    sourceSelected = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.layout = QVBoxLayout(self)

        self.group_box = QGroupBox("Select the Source Type")
        self.layout.addWidget(self.group_box)

        self.radio_buttons_layout = QVBoxLayout()
        # Existing options
        self.image_file_radio = QRadioButton("Image File")
        # New source options
        self.physical_drive_radio = QRadioButton("Physical Drive (not implemented)")
        self.logical_drive_radio = QRadioButton("Logical Drive (not implemented)")
        self.contents_of_folder_radio = QRadioButton("Contents of a Folder (not implemented)")

        # Add the radio buttons to the layout
        self.radio_buttons_layout.addWidget(self.image_file_radio)
        self.radio_buttons_layout.addWidget(self.physical_drive_radio)
        self.radio_buttons_layout.addWidget(self.logical_drive_radio)
        self.radio_buttons_layout.addWidget(self.contents_of_folder_radio)
        self.group_box.setLayout(self.radio_buttons_layout)

        self.next_button = QPushButton("Next")
        self.next_button.clicked.connect(self.on_next_clicked)
        self.layout.addWidget(self.next_button)

        self.close_button = QPushButton("Close")
        self.close_button.clicked.connect(lambda: self.window().close())
        self.layout.addWidget(self.close_button)

    def on_next_clicked(self):
        if self.image_file_radio.isChecked():
            self.sourceSelected.emit("conversion")
        elif self.contents_of_folder_radio.isChecked():
            self.sourceSelected.emit("folder_contents")
        elif self.physical_drive_radio.isChecked():
            self.sourceSelected.emit("physical_drive")
        elif self.logical_drive_radio.isChecked():
            self.sourceSelected.emit("logical_drive")


class ConversionWidget(QWidget):
    backRequested = Signal()  # Signal to request going back to the source selection

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setGeometry(100, 100, 400, 400)
        self.setWindowTitle("Convert E01 to DD/RAW")
        self.setWindowIcon(QIcon('Icons/logo.png'))
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout(self)

        form_layout = QFormLayout()
        self.input_line_edit = QLineEdit()
        browse_button = QPushButton("Browse...")
        browse_button.clicked.connect(self.browse_file)

        self.format_combo_box = QComboBox()
        self.format_combo_box.addItems(["DD", "RAW"])

        self.output_line_edit = QLineEdit()
        output_dir_button = QPushButton("Select Output Directory...")
        output_dir_button.clicked.connect(self.select_output_dir)

        form_layout.addRow(QLabel("Select E01 File:"), self.input_line_edit)
        form_layout.addRow(browse_button)
        form_layout.addRow(QLabel("Select Output Format:"), self.format_combo_box)
        form_layout.addRow(QLabel("Select Output Directory:"), self.output_line_edit)
        form_layout.addRow(output_dir_button)

        layout.addLayout(form_layout)

        # Buttons layout
        buttons_layout = QHBoxLayout()
        back_button = QPushButton("Back")
        back_button.clicked.connect(self.on_back_clicked)

        convert_button = QPushButton("Convert")
        convert_button.clicked.connect(self.convert)

        buttons_layout.addWidget(back_button)
        buttons_layout.addWidget(convert_button)

        layout.addLayout(buttons_layout)

    def on_back_clicked(self):
        main_window = self.parent().parent()
        main_window.show_select_source()

    def browse_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select E01 File", "", "E01 Files (*.e01)")
        if filename:
            self.input_line_edit.setText(filename)

    def select_output_dir(self):
        directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
        if directory:
            self.output_line_edit.setText(directory)

    def convert(self):
        input_path = self.input_line_edit.text()
        output_format = self.format_combo_box.currentText().lower()  # 'dd' or 'raw'
        output_dir = self.output_line_edit.text()

        if not input_path or not os.path.isfile(input_path):
            QMessageBox.warning(self, "Error", "The specified E01 file does not exist.")
            return

        if not output_dir or not os.path.isdir(output_dir):
            QMessageBox.warning(self, "Error", "The specified output directory does not exist.")
            return

        output_filename = f"{os.path.splitext(os.path.basename(input_path))[0]}.{output_format}"
        output_path = os.path.join(output_dir, output_filename)

        try:
            self.perform_conversion(input_path, output_path)
            QMessageBox.information(self, "Success", f"File has been successfully converted to {output_path}")
        except Exception as e:
            QMessageBox.critical(self, "Conversion Failed", f"An error occurred: {str(e)}")

    def perform_conversion(self, input_path, output_path):
        normalized_input_path = os.path.normpath(input_path)
        filenames = pyewf.glob(normalized_input_path)

        ewf_handle = pyewf.handle()
        ewf_handle.open(filenames)

        with open(output_path, 'wb') as output_file:
            buffer_size = ewf_handle.bytes_per_sector
            while True:
                data = ewf_handle.read(buffer_size)
                if not data:
                    break
                output_file.write(data)
        ewf_handle.close()


================================================
FILE: modules/exif_tab.py
================================================
from io import BytesIO as io_BytesIO

from PIL import Image
from PIL.ExifTags import TAGS
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit


class ExifViewerManager:
    def __init__(self):
        self.exif_data = None

    @staticmethod
    def get_exif_data_from_content(file_content):
        """Extract EXIF data from the given file content."""
        try:
            # Open the image from the given content
            image = Image.open(io_BytesIO(file_content))

            # Return None if the image format doesn't support EXIF
            if image.format != "JPEG":
                return None

            # Return the extracted EXIF data
            return image._getexif()
        except Exception as e:
            print(f"Error extracting EXIF data: {e}")
            return None

    def load_exif_data(self, file_content):
        """Load and process the EXIF data from the file content."""
        exif_data = self.get_exif_data_from_content(file_content)
        structured_data = []

        # If EXIF data is found, process it
        if exif_data:
            for key in exif_data.keys():
                if key in TAGS and isinstance(exif_data[key], (str, bytes)):
                    try:
                        tag_name = TAGS[key]
                        tag_value = exif_data[key]
                        structured_data.append((tag_name, tag_value))
                    except Exception as e:
                        print(f"Error processing key {key}: {e}")
            return structured_data
        else:
            return None


class ExifViewer(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Initialize the manager to handle EXIF data
        self.manager = ExifViewerManager()
        self.init_ui()

    def init_ui(self):
        """Initialize the user interface components."""
        # Set up a read-only text edit for displaying the EXIF data
        self.text_edit = QTextEdit(self)
        self.text_edit.setStyleSheet("border: 0px;")
        self.text_edit.setReadOnly(True)
        self.text_edit.setContentsMargins(0, 0, 0, 0)

        # Create the layout and add the text edit to it
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.text_edit)

        # Set the layout for the widget
        self.setLayout(layout)

    def display_exif_data(self, exif_data):
        """Display the provided EXIF data in the text edit."""
        if exif_data:
            # Format the EXIF data as an HTML table with CSS styling
            exif_table = f"""
                <style>
                    body {{
                        margin: 0;
                        padding: 0;
                        font-family: Arial, sans-serif;
                    }}
                    table {{
                        width: 100%;
                        border-collapse: collapse;
                        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
                    }}
                    td, th {{
                        border: 1px solid #ddd;
                        padding: 8px;
                        word-wrap: break-word;
                        text-align: left;
                    }}
                    th {{
                        background-color: #ddd;  /* Changed color to a light gray */
                        color: black;  /* Changed color to black */
                    }}
                    tr:nth-child(even) {{
                        background-color: #f2f2f2;
                    }}
                    tr:hover {{
                        background-color: #ddd;
                    }}
                </style>
                <table>
            """
            for key, value in exif_data:
                exif_table += f"<tr><td><b>{key}</b></td><td>{value}</td></tr>"
            exif_table += "</table>"
            self.text_edit.setHtml(exif_table)
        else:
            # Clear the text edit if there's no EXIF data to display
            self.text_edit.clear()

    def clear_content(self):
        """Clear the displayed content."""
        self.text_edit.clear()

    def load_and_display_exif_data(self, file_content):
        """Load the EXIF data from the file content and display it."""
        exif_data = self.manager.load_exif_data(file_content)
        self.display_exif_data(exif_data)


================================================
FILE: modules/file_carving.py
================================================
import datetime
import io
import os
import struct
import time
import zipfile
from concurrent.futures import ThreadPoolExecutor

import cv2
from PIL import Image, UnidentifiedImageError
from PIL.ExifTags import TAGS
from PyPDF2 import PdfReader
from PyPDF2.errors import PdfReadError
from PySide6.QtCore import QSize, QUrl, QRectF
from PySide6.QtCore import Qt
from PySide6.QtCore import Signal, Slot
from PySide6.QtGui import QIcon, QAction, QDesktopServices, QPixmap, QPainter, QImage
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import QListWidget, QListWidgetItem, QToolBar, QSizePolicy, QHBoxLayout, \
    QCheckBox, QHeaderView
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QTabWidget
from moviepy.editor import VideoFileClip
from pdf2image import convert_from_path


class NumericTableWidgetItem(QTableWidgetItem):
    def __lt__(self, other):
        self_value = self.text().split()[0]  # Extract numeric part of the text
        other_value = other.text().split()[0]  # Extract numeric part of the text
        self_unit = self.text().split()[1]  # Extract unit part of the text
        other_unit = other.text().split()[1]  # Extract unit part of the text
        units = {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}

        # Convert to bytes for comparison
        self_bytes = float(self_value) * (1024 ** units[self_unit])
        other_bytes = float(other_value) * (1024 ** units[other_unit])

        return self_bytes < other_bytes


class FileCarvingWidget(QWidget):
    file_carved = Signal(str, str, str, str, str)  # Unified signal for file carving

    def __init__(self, parent=None):
        super().__init__(parent)
        self.main_window = parent  # Store reference to MainWindow before it gets reparented by tab widget
        self.image_handler = None
        self.executor = ThreadPoolExecutor(max_workers=4)  # ThreadPoolExecutor for background tasks
        self.carved_files = []
        self.carved_file_names = set()  # Track carved file names to avoid duplicates
        self.allocation_map = []  # Map of allocated disk regions to skip during carving
        self.init_ui()

    def init_ui(self):
        self.layout = QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)  # Set the spacing to zero

        self.toolbar = QToolBar()
        self.toolbar.setContentsMargins(0, 0, 0, 0)
        self.layout.addWidget(self.toolbar)

        self.icon_label = QLabel()
        self.icon_label.setPixmap(QPixmap('Icons/icons8-carving-64.png'))
        self.icon_label.setFixedSize(48, 48)
        self.toolbar.addWidget(self.icon_label)

        self.title_label = QLabel("File Carving")
        self.title_label.setStyleSheet("""
            QLabel {
                font-size: 20px; /* Slightly larger size for the title */
                color: #37c6d0; /* Hex color for the text */
                font-weight: bold; /* Make the text bold */
                margin-left: 8px; /* Space between icon and label */
            }
        """)
        self.toolbar.addWidget(self.title_label)

        self.spacer = QLabel()
        self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.toolbar.addWidget(self.spacer)

        self.table_widget = self.create_table_widget()
        self.table_widget.resizeEvent = self.handle_resize_event

        self.list_widget = self.create_list_widget()

        self.fileTypeLayout = QHBoxLayout()
        self.fileTypes = {"All": QCheckBox("All"), "PDF": QCheckBox("PDF"), "JPG": QCheckBox("JPG"),
                          "PNG": QCheckBox("PNG"), "GIF": QCheckBox("GIF"), "WAV": QCheckBox("WAV"),
                          "MOV": QCheckBox("MOV"), "WMV": QCheckBox("WMV"), "ZIP": QCheckBox("ZIP"),
                          'BMP': QCheckBox("BMP")}

        for fileType, checkBox in self.fileTypes.items():
            self.fileTypeLayout.addWidget(checkBox)

        # Adding a widget to contain the file type checkboxes
        self.fileTypeWidget = QWidget()
        self.fileTypeWidget.setLayout(self.fileTypeLayout)
        self.toolbar.addWidget(self.fileTypeWidget)

        self.start_button = QPushButton("Start")
        self.start_button.clicked.connect(self.start_carving)

        self.toolbar.addWidget(self.start_button)

        self.stop_button = QPushButton("Stop")
        self.stop_button.clicked.connect(self.stop_carving)
        self.stop_button.setEnabled(False)

        self.toolbar.addWidget(self.stop_button)
        self.layout.addWidget(self.tab_widget)

        self.file_carved.connect(self.display_carved_file)

    def create_table_widget(self):
        table_widget = QTableWidget()
        table_widget.setColumnCount(6)  # Id, Name, Size, Type, Modification Date, File Path
        table_widget.setSelectionBehavior(QTableWidget.SelectRows)
        table_widget.setEditTriggers(QTableWidget.NoEditTriggers)
        table_widget.setSortingEnabled(True)
        table_widget.verticalHeader().setVisible(False)
        table_widget.setObjectName("fileCarvingTable")  # For CSS styling

        # Set size policy to expand with window
        table_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        # Use alternate row colors (matching Listing tab)
        table_widget.setAlternatingRowColors(True)
        table_widget.setIconSize(QSize(24, 24))

        # Enable horizontal scrolling for smaller windows (matching Listing tab)
        table_widget.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
        table_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        # Configure header - all columns use Interactive mode for horizontal scrolling
        header = table_widget.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.Interactive)  # Id - fixed, manually resizable
        header.setSectionResizeMode(1, QHeaderView.Interactive)  # Name - fixed, manually resizable
        header.setSectionResizeMode(2, QHeaderView.Interactive)  # Size - fixed, manually resizable
        header.setSectionResizeMode(3, QHeaderView.Interactive)  # Type - fixed, manually resizable
        header.setSectionResizeMode(4, QHeaderView.Interactive)  # Modification Date - fixed, manually resizable
        header.setSectionResizeMode(5, QHeaderView.Interactive)  # File Path - fixed, manually resizable

        # Set column widths (matching Listing tab style)
        table_widget.setColumnWidth(0, 100)   # Id - compact
        table_widget.setColumnWidth(1, 400)  # Name - widest (matching Listing tab)
        table_widget.setColumnWidth(2, 100)   # Size - compact (matching Listing tab)
        table_widget.setColumnWidth(3, 100)   # Type - compact (matching Listing tab)
        table_widget.setColumnWidth(4, 160)   # Modification Date - matching timestamp columns in Listing
        table_widget.setColumnWidth(5, 1100)  # File Path - wide (matching Listing tab)

        # Set header alignment (matching Listing tab)
        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        # Set the header labels
        table_widget.setHorizontalHeaderLabels(['Id', 'Name', 'Size', 'Type', 'Modification Date', 'File Path'])

        # Context menu and click handlers
        table_widget.setContextMenuPolicy(Qt.CustomContextMenu)
        table_widget.customContextMenuRequested.connect(self.open_context_menu)
        table_widget.cellClicked.connect(self.on_carved_file_clicked)

        self.tab_widget = QTabWidget()
        self.tab_widget.addTab(table_widget, "File List")
        return table_widget

    def create_list_widget(self):
        list_widget = QListWidget()
        list_widget.setViewMode(QListWidget.IconMode)
        list_widget.setIconSize(QSize(120, 120))
        list_widget.setResizeMode(QListWidget.Adjust)
        list_widget.setUniformItemSizes(True)
        list_widget.setSpacing(5)
        list_widget.setContextMenuPolicy(Qt.CustomContextMenu)
        list_widget.customContextMenuRequested.connect(self.open_context_menu)
        # Connect click event to open file in internal viewer
        list_widget.itemClicked.connect(self.on_carved_file_clicked)

        toolbar = QToolBar()

        # Define actions
        action_small_size = (QAction("Small Size", self))
        action_small_size.setIcon(QIcon('Icons/icons8-small-icons-50.png'))

        action_medium_size = (QAction("Medium Size", self))
        action_medium_size.setIcon(QIcon('Icons/icons8-medium-icons-50.png'))

        action_large_size = (QAction("Large Size", self))
        action_large_size.setIcon(QIcon('Icons/icons8-large-icons-50.png'))

        # Set icons

        # Connect actions to new slot methods
        action_small_size.triggered.connect(self.set_small_size)
        action_medium_size.triggered.connect(self.set_medium_size)
        action_large_size.triggered.connect(self.set_large_size)

        # Add actions to the toolbar
        toolbar.addAction(action_small_size)
        toolbar.addAction(action_medium_size)
        toolbar.addAction(action_large_size)

        # Create a layout and add the toolbar and the list widget to it
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(toolbar)
        layout.addWidget(list_widget)

        # Create a new widget, set its layout and add it to the tab widget
        widget = QWidget()
        widget.setLayout(layout)
        self.tab_widget.addTab(widget, "Thumbnails")
        return list_widget

    @staticmethod
    def center_crop_to_square(pixmap, target_size):
        """Crop pixmap to center square and scale to target size for uniform thumbnails."""
        if pixmap.isNull():
            return pixmap

        width = pixmap.width()
        height = pixmap.height()

        if width == height:
            # Already square, just scale
            return pixmap.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)

        # Determine the crop size (smaller dimension)
        crop_size = min(width, height)

        # Calculate crop position to center the crop
        x = (width - crop_size) // 2
        y = (height - crop_size) // 2

        # Crop to square
        cropped = pixmap.copy(x, y, crop_size, crop_size)

        # Scale to target size
        return cropped.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)

    @staticmethod
    def render_svg_to_pixmap(svg_path, target_size):
        """Render SVG file at target resolution for crisp icons."""
        renderer = QSvgRenderer(svg_path)
        if not renderer.isValid():
            return QPixmap()

        # Create QImage at target size with transparency
        image = QImage(target_size, target_size, QImage.Format_ARGB32)
        image.fill(Qt.transparent)

        # Render SVG onto the image
        painter = QPainter(image)
        renderer.render(painter, QRectF(0, 0, target_size, target_size))
        painter.end()

        # Convert QImage to QPixmap
        return QPixmap.fromImage(image)

    def set_icon_size(self, size):
        self.list_widget.setIconSize(QSize(size, size))
        for index in range(self.list_widget.count()):
            item = self.list_widget.item(index)
            item.setSizeHint(QSize(size + 10, size + 25))  # Compact padding with space for text

    def set_small_size(self):
        self.set_icon_size(80)

    def set_medium_size(self):
        self.set_icon_size(120)

    def set_large_size(self):
        self.set_icon_size(180)

    def start_carving(self):
        self.start_button.setEnabled(False)
        self.stop_button.setEnabled(True)
        self.clear_ui()
        self.carved_files.clear()
        self.carved_file_names.clear()

        # Ensure the 'carved_files' and 'thumbnails' directories exist
        if not os.path.exists("carved_files"):
            os.makedirs("carved_files")
        thumbnail_folder = os.path.join("carved_files", "thumbnails")
        if not os.path.exists(thumbnail_folder):
            os.makedirs(thumbnail_folder)

        # Build allocation map for all partitions to skip allocated files
        print("Building allocation map for allocated files...")
        self.allocation_map = []

        try:
            partitions = self.image_handler.get_partitions()

            if partitions:
                # Process each partition
                for partition_info in partitions:
                    # partition_info is (addr, desc, start, len)
                    start_offset = partition_info[2]  # start offset in sectors

                    # Build allocation map for this partition
                    partition_map = self.image_handler.build_allocation_map(start_offset)
                    self.allocation_map.extend(partition_map)
                    print(f"  Partition at offset {start_offset}: {len(partition_map)} allocated regions")
            else:
                # No partitions, try offset 0 (single filesystem)
                if self.image_handler.has_filesystem(0):
                    partition_map = self.image_handler.build_allocation_map(0)
                    self.allocation_map.extend(partition_map)
                    print(f"  Single filesystem: {len(partition_map)} allocated regions")

            # Sort the combined allocation map
            self.allocation_map.sort(key=lambda x: x[0])
            print(f"Total allocated regions to skip: {len(self.allocation_map)}")

        except Exception as e:
            print(f"Warning: Could not build allocation map: {e}")
            print("Will carve from entire disk (may include duplicates)")
            self.allocation_map = []

        selected_file_types = [fileType.lower() for fileType, checkbox in self.fileTypes.items() if
                               checkbox.isChecked()]
        self.executor.submit(self.carve_files, selected_file_types)

    def stop_carving(self):
        self.executor.shutdown(wait=True)  # Properly shutdown the executor
        self.start_button.setEnabled(True)  # Re-enable the start button
        self.stop_button.setEnabled(False)  # Disable the stop button

    def set_image_handler(self, image_handler):
        self.image_handler = image_handler
        self.start_button.setEnabled(True)

    @staticmethod
    def is_offset_allocated(offset, chunk_size, allocation_map):
        """
        Check if a given offset range overlaps with any allocated regions."""
        if not allocation_map:
            return False

        chunk_end = offset + chunk_size

        # Binary search to find potential overlapping regions
        # We need to check if our chunk [offset, chunk_end) overlaps with any allocated region
        left, right = 0, len(allocation_map)

        while left < right:
            mid = (left + right) // 2
            alloc_start, alloc_end = allocation_map[mid]

            # Check for overlap: two ranges overlap if one starts before the other ends
            if offset < alloc_end and chunk_end > alloc_start:
                return True

            # If our chunk is entirely before this allocated region, search left half
            if chunk_end <= alloc_start:
                right = mid
            # If our chunk is entirely after this allocated region, search right half
            else:
                left = mid + 1

        return False

    def open_context_menu(self, position):
        menu = QMenu()

        open_location_action = QAction("Open File Location")
        open_location_action.triggered.connect(self.open_file_location)

        open_image_action = QAction("Open Externally")
        open_image_action.triggered.connect(self.open_image)

        menu.addAction(open_location_action)
        menu.addAction(open_image_action)
        menu.exec_(self.table_widget.viewport().mapToGlobal(position))

    def open_image(self):
        if self.tab_widget.currentIndex() == 0:  # If the table tab is active
            current_item = self.table_widget.currentItem()
        else:  # If the thumbnail tab is active
            current_item = self.list_widget.currentItem()

        if current_item:
            file_name = current_item.text()
            for file_info in self.carved_files:
                if file_info[0] == file_name:
                    file_path = file_info[3]  # The file path is now at index 3
                    QDesktopServices.openUrl(QUrl.fromLocalFile(file_path))
                    break

    def open_file_location(self):
        current_item = self.list_widget.currentItem()
        if current_item:
            file_name = current_item.text()
            for file_info in self.carved_files:
                if file_info[0] == file_name:
                    file_path = file_info[3]  # The file path is now at index 3
                    QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(file_path)))
                    break

    def get_carved_timestamp(self, file_name):
        """Get the preserved timestamp for a carved file."""
        for file_info in self.carved_files:
            if file_info[0] == file_name:
                return file_info[4]  # Modification date at index 4
        return None

    def on_carved_file_clicked(self, *args):
        """Handle click on carved file to display in internal viewer.

        Reads file content directly from disk image (forensically sound) instead of
        from the carved file on disk. This ensures we're analyzing the original data.
        """
        # Get clicked file name (works for both table cellClicked and list itemClicked)
        if len(args) == 2:  # cellClicked(row, column) from table
            row = args[0]
            # Get file name from column 1 (Name column, column 0 is Id)
            file_name_item = self.table_widget.item(row, 1)
            if not file_name_item:
                return
            file_name = file_name_item.text()
        elif len(args) == 1:  # itemClicked(item) from list
            item = args[0]
            file_name = item.text()
        else:
            return

        # Find file info in carved_files list
        for file_info in self.carved_files:
            if file_info[0] == file_name:
                file_size_str = file_info[1]  # Size at index 1
                file_type = file_info[2]  # Type at index 2
                file_size = int(file_size_str)

                try:
                    # Extract disk offset from filename (hex format without extension)
                    offset_hex = os.path.splitext(file_name)[0]
                    offset = int(offset_hex, 16)

                    # Read file content directly from disk image (forensically sound!)
                    if not self.image_handler:
                        print("No image handler available")
                        return

                    file_content = self.image_handler.read(offset, file_size)
                    if not file_content:
                        print(f"Unable to read content from offset {hex(offset)}")
                        return

                    # Create data dict for viewer (matches mainwindow's format)
                    data = {
                        'name': file_name,
                        'size': file_size,
                        'type': file_type,
                        'offset': offset,
                        'is_carved': True,  # Flag indicating this is a carved file
                        'source': 'carved_file',
                        'file_content': file_content,  # Include content so metadata viewer doesn't re-read
                        'carved_timestamp': self.get_carved_timestamp(file_name)  # Get original timestamp if available
                    }

                    # Get MainWindow and call update_viewer_with_file_content
                    if self.main_window and hasattr(self.main_window, 'update_viewer_with_file_content'):
                        self.main_window.update_viewer_with_file_content(file_content, data)
                    else:
                        print("MainWindow not found or missing update_viewer_with_file_content method")

                except Exception as e:
                    print(f"Error opening carved file in viewer: {e}")
                    import traceback
                    traceback.print_exc()

                break

    def setup_buttons(self):
        self.start_button.setEnabled(False)
        self.start_button.clicked.connect(self.start_carving_thread)
        self.stop_button.setEnabled(False)
        self.stop_button.clicked.connect(self.stop_carving_thread)

    def start_carving_thread(self):
        self.start_button.setEnabled(False)
        self.stop_button.setEnabled(True)
        # Launch carving in a background thread
        self.executor.submit(self.carve_files)

    def stop_carving_thread(self):
        self.executor.shutdown(wait=False)
        self.start_button.setEnabled(True)
        self.stop_button.setEnabled(False)

    def is_valid_file(self, data, file_type):
        try:
            if file_type == 'pdf':
                # Validate PDF by trying to read it with PyPDF2
                PdfReader(io.BytesIO(data))
            elif file_type in ['jpg', 'jpeg', 'png', 'gif']:
                # Validate images by attempting to open them with PIL
                image = Image.open(io.BytesIO(data))
                image.verify()  # This will not load the image but only parse it
            elif file_type == 'bmp':
                return True
            elif file_type == 'wav':
                # Basic WAV validation could check for the RIFF header, file size, etc.
                if not data.startswith(b'RIFF') or not b'WAVE' in data[:12]:
                    return False
                # Additional WAV format checks could be implemented here
            elif file_type == 'mov':
                return True  # For now, we'll assume all MOV files are valid
            else:
                return True
            return True
        except (IOError, UnidentifiedImageError, PdfReadError, ValueError) as e:
            print(f"Error validating file of type {file_type}: {str(e)}")
            return False

    def carve_pdf_files(self, chunk, global_offset):
        pdf_start_signature = b'%PDF-'
        pdf_linearization_signature = b'/Linearized'
        pdf_end_signature = b'%%EOF'
        offset = 0
        while offset < len(chunk):
            start_index = chunk.find(pdf_start_signature, offset)
            if start_index == -1:
                break
            linearization_index = chunk.find(pdf_linearization_signature, start_index, start_index + 1024)
            if linearization_index != -1:
                file_size_start = chunk.find(b'/L ', linearization_index, linearization_index + 1024) + 3
                file_size_end = chunk.find(b'/', file_size_start)
                if file_size_end == -1:
                    file_size_end = chunk.find(b' ', file_size_start)
                if file_size_end != -1:
                    try:
                        file_size = int(chunk[file_size_start:file_size_end].split()[0])
                        pdf_content = chunk[start_index:start_index + file_size]
                        if self.is_valid_file(pdf_content, 'pdf'):
                            self.save_file(pdf_content, 'pdf', global_offset + start_index, file_size)
                            offset = start_index + file_size
                            continue
                    except ValueError:
                        pass
            end_index = chunk.find(pdf_end_signature, start_index)
            if end_index != -1:
                end_index += len(pdf_end_signature)
                pdf_content = chunk[start_index:end_index]
                if self.is_valid_file(pdf_content, 'pdf'):
                    self.save_file(pdf_content, 'pdf', global_offset + start_index, end_index - start_index)
                offset = end_index
            else:
                offset = start_index + 1

    def carve_wav_files(self, chunk, offset):
        wav_start_signature = b'RIFF'
        offset = 0
        while offset < len(chunk):
            start_index = chunk.find(wav_start_signature, offset)
            if start_index == -1:
                break

            if chunk[start_index + 8:start_index + 12] != b'WAVE':
                offset = start_index + 4
                continue

            file_size_bytes = chunk[start_index + 4:start_index + 8]
            file_size = int.from_bytes(file_size_bytes, byteorder='little') + 8

            if start_index + file_size > len(chunk):
                wav_content = chunk[start_index:]
                offset = len(chunk)
            else:
                wav_content = chunk[start_index:start_index + file_size]
                offset = start_index + file_size

            if self.is_valid_file(wav_content, 'wav'):
                self.save_file(wav_content, 'wav', 'carved_files', start_index)

    def carve_mov_files(self, chunk, offset):
        mov_signatures = [
            # b'ftyp', b'moov', b'mdat', #b'pnot', b'udta', #b'uuid',
            # b'moof', b'free', b'skip', b'jP2 ', b'wide', b'load',
            # b'ctab', b'imap', b'matt', b'kmat', b'clip', b'crgn',
            # b'sync', b'chap', b'tmcd', b'scpt', b'ssrc', b'PICT'
            b'moov', b'mdat', b'free', b'wide'
        ]

        mov_file_found = False
        mov_data = b''
        mov_file_offset = offset
        mov_file_size = 0

        while offset < len(chunk):
            if offset + 8 > len(chunk):
                # Not enough data for an atom header
                break

            atom_size = int.from_bytes(chunk[offset:offset + 4], 'big')
            atom_type = chunk[offset + 4:offset + 8]

            if atom_type not in mov_signatures:
                if mov_file_found:
                    # End of MOV file
                    break
                else:
                    # Not a MOV file or just a stray header, skip ahead
                    offset += 4
                    continue

            mov_file_found = True
            mov_file_size += atom_size

            if offset + atom_size > len(chunk):
                # Atom extends beyond this chunk, store what we have and wait for more data
                mov_data += chunk[offset:]
                break
            else:
                # We have the whole atom, store it
                mov_data += chunk[offset:offset + atom_size]

            offset += atom_size

        if mov_file_found and mov_data:
            # file_name = f"carved_{mov_file_offset}.mov"
            # file_path = os.path.join("carved_files", file_name)
            # self.save_file(mov_data, 'mov', file_path)
            self.save_file(mov_data, 'mov', 'carved_files', mov_file_offset)

    def carve_jpg_files(self, chunk, offset):
        jpg_start_signature = b'\xFF\xD8\xFF'
        jpg_end_signature = b'\xFF\xD9'
        offset = 0
        while offset < len(chunk):
            start_index = chunk.find(jpg_start_signature, offset)
            if start_index == -1:
                break

            end_index = chunk.find(jpg_end_signature, start_index)
            if end_index != -1:
                jpg_content = chunk[start_index:end_index + len(jpg_end_signature)]

                # Check if it's a valid JPG file
                if self.is_valid_file(jpg_content, 'jpg'):
                    self.save_file(jpg_content, 'jpg', 'carved_files', start_index)

                offset = end_index + len(jpg_end_signature)
            else:
                offset = start_index + 1  # Continue searching

    def carve_gif_files(self, chunk, offset):
        gif_start_signature = b'\x47\x49\x46\x38'
        gif_end_signature = b'\x00\x3B'
        offset = 0
        while offset < len(chunk):
            start_index = chunk.find(gif_start_signature, offset)
            if start_index == -1:
                break

            end_index = chunk.find(gif_end_signature, start_index)
            if end_index != -1:
                gif_content = chunk[start_index:end_index + len(gif_end_signature)]

                # Check if it's a valid GIF file
                if self.is_valid_file(gif_content, 'gif'):
                    self.save_file(gif_content, 'gif', 'carved_files', start_index)

                offset = end_index + len(gif_end_signature)
            else:
                offset = start_index + 1

    def carve_png_files(self, chunk, offset):
        png_start_signature = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
        png_end_signature = b'\x49\x45\x4E\x44\xAE\x42\x60\x82'
        offset = 0
        while offset < len(chunk):
            start_index = chunk.find(png_start_signature, offset)
            if start_index == -1:
                break

            end_index = chunk.find(png_end_signature, start_index)
            if end_index != -1:
                png_content = chunk[start_index:end_index + len(png_end_signature)]

                # Check if it's a valid PNG file
                if self.is_valid_file(png_content, 'png'):
                    self.save_file(png_content, 'png', 'carved_files', start_index)

                offset = end_index + len(png_end_signature)
            else:
                offset = start_index + 1

    def carve_wmv_files(self, chunk, offset):
        # Define ASF header signature
        asf_header_signature = b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C'

        current_offset = 0

        while current_offset < len(chunk):
            # Search for ASF header
            start_index = chunk.find(asf_header_signature, current_offset)
            if start_index == -1:
                break

            # Find the file properties object header within the first 512 bytes of the file
            max_search_size = min(start_index + 512, len(chunk))
            file_properties_header = b'\xA1\xDC\xAB\x8C\x47\xA9\xCF\x11\x8E\xE4\x00\xC0\x0C\x20\x53\x65'
            file_properties_index = chunk.find(file_properties_header, start_index, max_search_size)
            if file_properties_index == -1:
                current_offset = start_index + 1
                continue

            # Extract the file size located at offset 40 within the object
            file_size_offset = file_properties_index + 40
            file_size_bytes = chunk[file_size_offset:file_size_offset + 8]
            file_size = int.from_bytes(file_size_bytes, byteorder='little')

            # Calculate end index based on file size
            end_index = start_index + file_size

            # Extract WMV content
            wmv_content = chunk[start_index:end_index]

            # Save the WMV content directly into the carved_files directory
            self.save_file(wmv_content, 'wmv', 'carved_files', start_index + offset)
            current_offset = end_index

    def carve_zip_files(self, chunk, global_offset):
        # Define ZIP header signatures
        local_file_header_signature = b'\x50\x4b\x03\x04'
        end_of_central_dir_signature = b'\x50\x4b\x05\x06'

        current_pos = 0
        zip_file_parts = []  # List to hold all parts of the ZIP file

        while current_pos < len(chunk):
            # Search for local file header
            local_header_index = chunk.find(local_file_header_signature, current_pos)
            if local_header_index == -1:
                break

            # Extract compressed size from local file header
            compressed_size = struct.unpack("<I", chunk[local_header_index + 18:local_header_index + 22])[0]

            # Calculate next local file header index
            next_local_header_index = local_header_index + 30 + compressed_size

            # Extract file content
            file_content = chunk[local_header_index:next_local_header_index]
            zip_file_parts.append(file_content)  # Add the file content to the ZIP parts list

            # Move to next local file header
            current_pos = next_local_header_index

        # Now, find and append the Central Directory and End of Central Directory Record
        end_central_dir_index = chunk.find(end_of_central_dir_signature, current_pos)
        if end_central_dir_index != -1:
            # Extract comment length and calculate the total end of the ZIP file structure
            comment_length = struct.unpack("<H", chunk[end_central_dir_index + 20:end_central_dir_index + 22])[0]
            zip_end = end_central_dir_index + 22 + comment_length

            # Extract the Central Directory and End of Central Directory Record
            zip_file_structure = chunk[current_pos:zip_end]
            zip_file_parts.append(zip_file_structure)  # Add this to the ZIP parts list

        # Combine all parts into a single ZIP file content
        if zip_file_parts:
            complete_zip_file_content = b''.join(zip_file_parts)
            self.save_file(complete_zip_file_content, 'zip', 'carved_files', global_offset)

        return None

    def carve_bmp_files(self, chunk, offset):
        bmp_start_signature = b'BM'  # BMP files start with 'BM'
        header_size = 14  # The static header size for BMP files

        current_offset = 0
        while current_offset < len(chunk) - header_size:
            # Look for the BMP signature
            start_index = chunk.find(bmp_start_signature, current_offset)
            if start_index == -1:
                break  # No more BMP files found

            # Verify there's enough chunk left to read the BMP size
            if start_index + header_size > len(chunk) - 4:
                break  # Not enough data for size

            # Read file size directly from header
            bmp_file_size = int.from_bytes(chunk[start_index + 2:start_index + 6], byteorder='little')

            # Sanity check for BMP size (adjust max and min size as per your need)
            if bmp_file_size < 100 or bmp_file_size > 5000000:
                current_offset = start_index + 2
                continue  # Not a valid BMP size, skip to next possible start

            # Read and check dimensions for further validation
            bmp_width = int.from_bytes(chunk[start_index + 18:start_index + 22], byteorder='little')
            bmp_height = int.from_bytes(chunk[start_index + 22:start_index + 26], byteorder='little')

            # Reasonable dimensions check (adjust max width/height as per your need)
            if bmp_width <= 0 or bmp_width > 10000 or bmp_height <= 0 or bmp_height > 10000:
                current_offset = start_index + 2
                continue  # Unreasonable dimensions, likely not a BMP

            # Extract the BMP file if it's entirely within the chunk
            if start_index + bmp_file_size <= len(chunk):
                bmp_content = chunk[start_index:start_index + bmp_file_size]
                self.save_file(bmp_content, 'bmp', 'carved_files', start_index + offset)
                current_offset = start_index + bmp_file_size  # Move past this BMP file
            else:
                break  # The BMP file exceeds the chunk boundary, stop processing

        # Return if more data is needed or if processing is complete
        return None

    def carve_files(self, selected_file_types):
        try:
            self.stop_carving = False
            chunk_size = 1024 * 1024 * 100
            offset = 0
            chunks_processed = 0
            chunks_skipped = 0

            while offset < self.image_handler.get_size():
                # Check if this chunk overlaps with allocated space
                if self.is_offset_allocated(offset, chunk_size, self.allocation_map):
                    # Skip this chunk - it's in allocated space (existing files)
                    chunks_skipped += 1
                    offset += chunk_size
                    continue

                chunks_processed += 1

                chunk = self.image_handler.read(offset, chunk_size)
                if not chunk:
                    break

                if self.stop_carving:
                    self.stop_carving = False
                    self.start_button.setEnabled(True)
                    self.stop_button.setEnabled(False)
                    print(f"Carving stopped. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks")
                    return

                # Call the carve function for each selected file type
                for file_type in selected_file_types:
                    if file_type == 'all':
                        self.carve_wav_files(chunk, offset)
                        self.carve_mov_files(chunk, offset)
                        self.carve_pdf_files(chunk, offset)
                        self.carve_jpg_files(chunk, offset)
                        self.carve_gif_files(chunk, offset)
                        self.carve_png_files(chunk, offset)
                        self.carve_wmv_files(chunk, offset)
                        self.carve_zip_files(chunk, offset)
                        self.carve_bmp_files(chunk, offset)
                    elif file_type == 'wav':
                        self.carve_wav_files(chunk, offset)
                    elif file_type == 'mov':
                        self.carve_mov_files(chunk, offset)
                    elif file_type == 'pdf':
                        self.carve_pdf_files(chunk, offset)
                    elif file_type == 'jpg':
                        self.carve_jpg_files(chunk, offset)
                    elif file_type == 'gif':
                        self.carve_gif_files(chunk, offset)
                    elif file_type == 'png':
                        self.carve_png_files(chunk, offset)
                    elif file_type == 'wmv':
                        self.carve_wmv_files(chunk, offset)
                    elif file_type == 'zip':
                        self.carve_zip_files(chunk, offset)
                    elif file_type == 'bmp':
                        self.carve_bmp_files(chunk, offset)

                offset += chunk_size

            print(f"Carving complete. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks")
        finally:
            self.start_button.setEnabled(True)
            self.stop_button.setEnabled(False)

    @staticmethod
    def extract_original_timestamp(file_content, file_type):
        """Extract original file timestamp from file headers/metadata.

        Returns:
            datetime object if timestamp found, None otherwise
        """
        try:
            if file_type.lower() in ['jpg', 'jpeg', 'png']:
                # Extract EXIF DateTimeOriginal from images
                try:
                    img = Image.open(io.BytesIO(file_content))
                    exif_data = img._getexif()
                    if exif_data:
                        # Look for DateTimeOriginal (tag 36867) or DateTime (tag 306)
                        for tag_id, value in exif_data.items():
                            tag_name = TAGS.get(tag_id, tag_id)
                            if tag_name in ['DateTimeOriginal', 'DateTime']:
                                # Parse format: "2024:01:15 14:30:00"
                                return datetime.datetime.strptime(str(value), '%Y:%m:%d %H:%M:%S')
                except Exception:
                    pass

            elif file_type.lower() == 'pdf':
                # Extract CreationDate from PDF metadata
                try:
                    pdf = PdfReader(io.BytesIO(file_content))
                    if pdf.metadata and '/CreationDate' in pdf.metadata:
                        date_str = pdf.metadata['/CreationDate']
                        # PDF date format: "D:20240115143000"
                        if date_str.startswith('D:'):
                            date_str = date_str[2:16]  # Extract YYYYMMDDHHmmss
                            return datetime.datetime.strptime(date_str, '%Y%m%d%H%M%S')
                except Exception:
                    pass

            elif file_type.lower() == 'zip':
                # Extract timestamp from ZIP central directory
                try:
                    with zipfile.ZipFile(io.BytesIO(file_content)) as zf:
                        if zf.namelist():
                            # Get timestamp of first file in archive
                            first_file_info = zf.getinfo(zf.namelist()[0])
                            return datetime.datetime(*first_file_info.date_time)
                except Exception:
                    pass

        except Exception as e:
            print(f"Error extracting timestamp for {file_type}: {e}")

        return None

    def save_file(self, file_content, file_type, file_path, offset):
        # Ensure the 'carved_files' directory exists
        if not os.path.exists("carved_files"):
            os.makedirs("carved_files")

        offset_hex = format(offset, 'x')
        file_name = f"{offset_hex}.{file_type}"
        file_path = os.path.join("carved_files", file_name)

        # Write file content to disk
        with open(file_path, "wb") as f:
            f.write(file_content)

        # Try to extract original timestamp from file metadata
        original_timestamp = self.extract_original_timestamp(file_content, file_type)

        if original_timestamp:
            # Convert datetime to timestamp (seconds since epoch)
            timestamp = time.mktime(original_timestamp.timetuple())
            # Set both access time and modification time to preserve original timestamp
            os.utime(file_path, (timestamp, timestamp))
            modification_date = original_timestamp.strftime("%Y-%m-%d %H:%M:%S")
        else:
            # Fall back to carving time if no original timestamp found
            modification_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        file_size = str(len(file_content))
        self.carved_files.append((file_name, file_size, file_type, file_path, modification_date))
        self.file_carved.emit(file_name, file_size, file_type, modification_date, file_path)
        self.carved_file_names.add(file_name)

    @Slot(str, str, str, str, str)
    def display_carved_file(self, name, size, type_, modification_date, file_path):
        row = self.table_widget.rowCount()
        readable_size = self.image_handler.get_readable_size(int(size))
        self.table_widget.insertRow(row)

        # Get file icon based on type/extension
        extension = type_.lower() if type_ else 'unknown'
        icon_path = self.main_window.db_manager.get_icon_path('file', extension)

        # Set Id column
        self.table_widget.setItem(row, 0, QTableWidgetItem(str(row + 1)))

        # Set Name column with icon
        name_item = QTableWidgetItem(name)
        name_item.setIcon(QIcon(icon_path))
        self.table_widget.setItem(row, 1, name_item)

        # Set other columns
        self.table_widget.setItem(row, 2, NumericTableWidgetItem(readable_size))
        self.table_widget.setItem(row, 3, QTableWidgetItem(type_))
        self.table_widget.setItem(row, 4, QTableWidgetItem(modification_date))
        self.table_widget.setItem(row, 5, QTableWidgetItem(file_path))

        # Only proceed if the file type is one of the supported formats
        if type_.lower() in ['jpg', 'jpeg', 'png', 'gif', 'mov', 'pdf', 'wmv', 'bmp', 'zip', 'wav']:
            file_full_path = os.path.join("carved_files", name)
            thumbnail_folder = os.path.join("carved_files", "thumbnails")  # Folder to save thumbnails

            if not os.path.exists(thumbnail_folder):
                os.makedirs(thumbnail_folder)  # Create the thumbnail folder if it doesn't exist

            if type_.lower() == 'mov':
                thumbnail_path = os.path.join(thumbnail_folder, name.replace('.mov', '.png'))
                with VideoFileClip(file_full_path) as clip:
                    clip.save_frame(thumbnail_path, t=0.5)  # save frame at 0.5 seconds
                pixmap = QPixmap(thumbnail_path)

            elif type_.lower() == 'pdf':
                # Convert the first page of the PDF to a thumbnail
                images = convert_from_path(file_full_path)
                thumbnail_path = os.path.join(thumbnail_folder, name.replace('.pdf', '.png'))
                images[0].save(thumbnail_path, 'PNG')
                # Create the QPixmap from the full path
                pixmap = QPixmap(thumbnail_path)

            elif type_.lower() == 'wmv':
                capture = cv2.VideoCapture(file_full_path)
                success, image = capture.read()
                capture.release()  # Release the capture object explicitly
                if success:
                    thumbnail_path = os.path.join(thumbnail_folder, name.replace('.wmv', '.png'))
                    cv2.imwrite(thumbnail_path, image)
                    pixmap = QPixmap(thumbnail_path)
                else:
                    print("Failed to extract thumbnail from WMV file")

            elif type_.lower() == 'zip':
                # Render ZIP icon at target size for crisp display
                pixmap = self.render_svg_to_pixmap('Icons/mimetypes/application-zip.svg', 120)

            elif type_.lower() == 'wav':
                # Render audio icon at target size for crisp display
                pixmap = self.render_svg_to_pixmap('Icons/mimetypes/audio-x-generic.svg', 120)

            else:
                # For image files, use the original file path
                thumbnail_path = file_full_path
                pixmap = QPixmap(thumbnail_path)

            # Center-crop to perfect square for modern uniform gallery look (skip for SVG icons)
            if type_.lower() not in ['zip', 'wav']:
                pixmap = self.center_crop_to_square(pixmap, 120)
            icon = QIcon(pixmap)

            # Create a QListWidgetItem, set its icon, and provide a size hint to ensure the text is visible
            item = QListWidgetItem(icon, name)
            # Set a compact size for the QListWidgetItem with minimal padding for text
            item.setSizeHint(QSize(130, 145))

            # Set the item flags to not be movable and to be selectable
            item.setFlags(item.flags() & ~Qt.ItemIsDragEnabled & ~Qt.ItemIsDropEnabled)

            # Add the QListWidgetItem to the list widget
            self.list_widget.addItem(item)

    def clear(self):
        self.table_widget.setRowCount(0)
        self.list_widget.clear()
        self.carved_files.clear()
        self.start_button.setEnabled(True)
        self.stop_button.setEnabled(False)

    def clear_ui(self):
        self.table_widget.setRowCount(0)
        self.list_widget.clear()

    def handle_resize_event(self, event):
        # Calculate total width of the table
        total_width = self.table_widget.width()

        # Fixed columns: Id, Size, Type, Modification Date
        fixed_width = (self.table_widget.columnWidth(0) +  # Id
                       self.table_widget.columnWidth(2) +  # Size
                       self.table_widget.columnWidth(3) +  # Type
                       self.table_widget.columnWidth(4))  # Modification Date

        # Remaining space for dynamic columns
        remaining_width = total_width - fixed_width

        # Allocate remaining space proportionally
        self.table_widget.setColumnWidth(1, remaining_width // 2)  # Name column
        self.table_widget.setColumnWidth(5, remaining_width // 2)  # File Path column

        super(QTableWidget, self.table_widget).resizeEvent(event)


================================================
FILE: modules/hex_tab.py
================================================
import os
from functools import lru_cache

from PySide6.QtCore import Qt, QObject, Signal, QThread, QSize
from PySide6.QtGui import QAction, QIcon, QFont, QResizeEvent
from PySide6.QtWidgets import (QToolBar, QLabel, QMessageBox, QWidget, QVBoxLayout,
                               QLineEdit, QTableWidget, QHeaderView, QTableWidgetItem, QListWidget,
                               QSizePolicy, QFrame, QApplication, QMenu, QAbstractItemView, QFileDialog,
                               QToolButton, QComboBox, QSplitter)


class SearchWorker(QObject):
    search_finished = Signal(list)

    def __init__(self, hex_viewer_manager, query):
        super().__init__()
        self.hex_viewer_manager = hex_viewer_manager
        self.query = query

    def run(self):
        matches = self.hex_viewer_manager.search(self.query)
        self.search_finished.emit(matches)


class HexViewerManager:
    LINES_PER_PAGE = 1024

    def __init__(self, hex_content, byte_content):
        self.hex_content = hex_content
        self.byte_content = byte_content
        self.num_total_pages = (len(hex_content) // 32) // self.LINES_PER_PAGE
        if (len(hex_content) // 32) % self.LINES_PER_PAGE:
            self.num_total_pages += 1

    @lru_cache(maxsize=None)
    def format_hex(self, page=0):
        start_index = page * self.LINES_PER_PAGE * 32
        end_index = start_index + (self.LINES_PER_PAGE * 32)
        lines = []
        chunk_starts = range(start_index, end_index, 32)
        for start in chunk_starts:
            if start >= len(self.hex_content):
                break
            lines.append(self.format_hex_chunk(start))
        return '\n'.join(lines)

    def format_hex_chunk(self, start):
        hex_part = []
        ascii_repr = []
        for j in range(start, start + 32, 2):
            chunk = self.hex_content[j:j + 2]
            if not chunk:
                break
            chunk_int = int(chunk, 16)
            hex_part.append(chunk.upper())
            ascii_repr.append(chr(chunk_int) if 32 <= chunk_int <= 126 else '.')
        hex_line = ' '.join(hex_part)
        padding = ' ' * (48 - len(hex_line))
        ascii_line = ''.join(ascii_repr)
        line = f'0x{start // 2:08x}: {hex_line}{padding}  {ascii_line}'
        return line

    def total_pages(self):
        return self.num_total_pages

    def search(self, query):
        if all(part.isalnum() or part.isspace() for part in query.split()):
            try:
                query_bytes = bytes.fromhex(query.replace(" ", ""))
                return self.search_by_hex(query_bytes)
            except ValueError:
                pass  # Invalid hex value

        if query.startswith("0x"):
            return self.search_by_address(query)
        else:
            return self.search_by_string(query)

    def search_by_address(self, address):
        """Searches for the line that contains the given address (offset)"""
        try:
            address_int = int(address, 16)
            line_number = address_int // 16
            if 0 <= line_number < len(self.byte_content) // 16:
                return [line_number]
            else:
                return []
        except ValueError:
            return []

    def search_by_string(self, query):
        # Implementation for searching by string
        matches = []
        query_bytes = query.encode('utf-8')

        start = 0
        while start < len(self.byte_content):
            position = self.byte_content.find(query_bytes, start)
            if position == -1:
                break
            start = position + 1  # Move the start to the next character
            line_number = position // 16  # Calculate line number
            matches.append(line_number)

        return matches

    def search_by_hex(self, hex_query):
        if all(part.isalnum() for part in hex_query.split()):
            try:
                query_bytes = bytes.fromhex(hex_query.replace(" ", ""))
            except ValueError:
                return []  # Invalid hex value
        else:
            return []  # Non-alphanumeric characters in the query

        matches = []
        start = 0
        while start < len(self.byte_content):
            position = self.byte_content[start:].find(query_bytes)
            if position == -1:
                break
            start += position  # Adjust the start to the found position
            line_number = start // 16  # Calculate line number
            matches.append(line_number)
            start += len(query_bytes)  # Move past the current match
        return matches


class HexViewer(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.hex_viewer_manager = None
        self.current_page = 0

        self.context_menu = QMenu(self)
        self.copy_action = QAction("Copy", self)
        self.copy_action.triggered.connect(self.copy_to_clipboard)
        self.context_menu.addAction(self.copy_action)

        # Set up a context menu event handler
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)

        self.initialize_ui()

    def show_context_menu(self, pos):
        # Show the context menu at the cursor position
        self.context_menu.exec_(self.mapToGlobal(pos))

    def copy_to_clipboard(self):
        selected_text = ""

        # Check if any cells in the hex_table are selected
        selected_indexes = self.hex_table.selectedIndexes()
        if selected_indexes:
            # Sort the selected indexes by row
            selected_indexes.sort(key=lambda index: index.row())

            for i, index in enumerate(selected_indexes):
                selected_text += index.data(Qt.DisplayRole)

                if index.column() == 16:  # The last column (ASCII), add a newline
                    selected_text += "\n"
                else:
                    next_index = selected_indexes[i + 1] if i + 1 < len(selected_indexes) else None

                    # Add a space if the next cell is in the same row
                    if next_index and next_index.row() == index.row():
                        selected_text += " "

        # Copy the selected text to the clipboard
        if selected_text:
            clipboard = QApplication.clipboard()
            clipboard.setText(selected_text)

    def initialize_ui(self):
        self.layout = QVBoxLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.layout.setAlignment(Qt.AlignCenter)

        self.setup_toolbar()
        self.layout.addWidget(self.toolbar)

        # Create a QSplitter for dynamic resizing
        self.splitter = QSplitter(Qt.Horizontal, self)  # Horizontal splitter for hex_table and search_results_frame

        # Setup Hex Table
        self.setup_hex_table()
        self.hex_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)  # Make hex table expandable

        # Add the hex table to the splitter
        self.splitter.addWidget(self.hex_table)

        # Create a QVBoxLayout for the search results and its title
        self.search_results_layout = QVBoxLayout()
        self.search_results_layout.setContentsMargins(2, 2, 2, 2)
        self.search_results_layout.setSpacing(2)

        self.search_results_frame = QFrame(self)  # This frame will contain the title and the search results
        self.search_results_frame.setMaximumWidth(180)  # Make it narrower
        self.search_results_frame.setObjectName("search_results_frame")  # Set object name for stylesheet targeting
        self.search_results_frame.setSizePolicy(QSizePolicy.Fixed,
                                                QSizePolicy.Expanding)  # Fixed width, expandable height

        self.search_results_title = QLabel("Search Results", self.search_results_frame)
        self.search_results_title.setAlignment(Qt.AlignCenter)
        self.search_results_title.setFixedHeight(22)
        self.search_results_title.setObjectName("search_results_title")  # Set object name for stylesheet targeting
        self.search_results_layout.addWidget(self.search_results_title)

        self.search_results_widget = QListWidget(self.search_results_frame)
        self.search_results_widget.setObjectName("search_results_widget")  # Set object name for stylesheet targeting
        self.search_results_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)  # Show scroll bar when needed
        self.search_results_widget.itemClicked.connect(self.search_result_clicked)
        self.search_results_widget.setFont(QFont("Courier", 9))  # Smaller font
        self.search_results_layout.addWidget(self.search_results_widget)

        self.search_results_frame.setLayout(self.search_results_layout)

        # Add the search results frame to the splitter
        self.splitter.addWidget(self.search_results_frame)

        # Set both widgets to expand in both directions
        self.splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        # Add the splitter to the main layout
        self.layout.addWidget(self.splitter)

        # Set the main layout
        self.setLayout(self.layout)

    def resizeEvent(self, event: QResizeEvent):
        """Handle window resizing to update layout."""
        # Adjust splitter sizes dynamically based on new window dimensions
        total_width = event.size().width()
        total_height = event.size().height()

        # Set sizes for horizontal splitter: 75% for hex_table and 25% for search_results_frame
        self.splitter.setSizes([int(total_width * 0.75), int(total_width * 0.25)])

        super().resizeEvent(event)

    def setup_toolbar(self):
        self.toolbar = QToolBar(self)
        self.toolbar.setContentsMargins(0, 0, 0, 0)
        self.toolbar.setMovable(False)
        self.toolbar.setIconSize(QSize(16, 16))  # Reduce icon size
        self.toolbar.setFixedHeight(32)  # Reduce toolbar height
        self.toolbar.setStyleSheet("""
            QToolBar {
                spacing: 2px;
                padding: 1px;
            }
            QToolButton {
                padding: 2px;
                margin: 1px;
            }
        """)
        # disable right click
        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)

        # Navigation buttons
        self.first_action = QAction(QIcon("Icons/icons8-thick-arrow-pointing-up-50.png"), "First", self)
        self.first_action.triggered.connect(self.load_first_page)
        self.toolbar.addAction(self.first_action)

        self.prev_action = QAction(QIcon("Icons/icons8-left-arrow-50.png"), "Previous", self)
        self.prev_action.triggered.connect(self.previous_page)
        self.toolbar.addAction(self.prev_action)

        # Page entry
        self.page_entry = QLineEdit(self)
        self.page_entry.setMaximumWidth(40)
        self.page_entry.setFixedHeight(25)  # Set fixed height for input
        self.page_entry.setPlaceholderText("1")
        self.page_entry.returnPressed.connect(self.go_to_page_by_entry)
        self.toolbar.addWidget(self.page_entry)

        # Total pages label
        self.total_pages_label = QLabel(" of ")
        self.total_pages_label.setFixedHeight(25)  # Set fixed height for label
        self.toolbar.addWidget(self.total_pages_label)

        self.next_action = QAction(QIcon("Icons/icons8-right-arrow-50.png"), "Next", self)
        self.next_action.triggered.connect(self.next_page)
        self.toolbar.addAction(self.next_action)

        self.last_action = QAction(QIcon("Icons/icons8-down-50.png"), "Last", self)
        self.last_action.triggered.connect(self.load_last_page)
        self.toolbar.addAction(self.last_action)

        # Add a small spacer
        spacer = QWidget(self)
        spacer.setFixedSize(20, 0)
        self.toolbar.addWidget(spacer)

        # Add a QLabel and a QComboBox for font size to the toolbar
        font_label = QLabel("Font Size: ")
        font_label.setFixedHeight(25)  # Set fixed height for label
        self.toolbar.addWidget(font_label)

        self.font_size_combobox = QComboBox(self)
        self.font_size_combobox.setFixedHeight(25)  # Set fixed height for combobox
        self.font_size_combobox.setFixedWidth(60)  # Increase width to show full numbers
        self.font_size_combobox.addItems(["8", "10", "12", "14", "16", "18", "20", "24", "28", "32", "36"])
        self.font_size_combobox.currentTextChanged.connect(self.update_font_size)
        self.toolbar.addWidget(self.font_size_combobox)

        # Add small spacer
        spacer = QWidget(self)
        spacer.setFixedSize(20, 0)
        self.toolbar.addWidget(spacer)

        self.export_button = QToolButton(self)
        self.export_button.setObjectName("exportButton")  # Assign a unique object name
        self.export_button.setText("Export")
        self.export_button.setToolButtonStyle(Qt.ToolButtonTextOnly)  # Change to text only since no icon is used
        self.export_button.setFixedHeight(25)  # Set fixed height
        self.export_button.setFixedWidth(100)  # Set fixed width to show full text
        self.export_button.setPopupMode(QToolButton.MenuButtonPopup)  # Set the popup mode

        # Add format options to the menu
        self.export_menu = QMenu(self)

        self.text_format_action = QAction("Text (.txt)", self)
        self.text_format_action.triggered.connect(lambda: self.export_content("txt"))
        self.export_menu.addAction(self.text_format_action)

        self.html_format_action = QAction("HTML (.html)", self)
        self.html_format_action.triggered.connect(lambda: self.export_content("html"))
        self.export_menu.addAction(self.html_format_action)

        self.export_button.setMenu(self.export_menu)
        self.toolbar.addWidget(self.export_button)

        # Add a spacer to push the following widgets to the right
        spacer = QWidget(self)
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.toolbar.addWidget(spacer)

        # Search bar components
        self.search_bar = QLineEdit(self)
        self.search_bar.setMaximumWidth(180)  # Reduce width to save space
        self.search_bar.setFixedHeight(25)  # Reduce height
        self.search_bar.setContentsMargins(5, 0, 5, 0)  # Reduce margins
        self.search_bar.setPlaceholderText("Search...")
        self.search_bar.returnPressed.connect(self.trigger_search)
        self.toolbar.addWidget(self.search_bar)

    def update_font_size(self):
        # Get the current font size from the combobox
        selected_size = int(self.font_size_combobox.currentText())

        # Set the new font size to the hex_table
        current_font = self.hex_table.font()
        current_font.setPointSize(selected_size)
        self.hex_table.setFont(current_font)

        # Dynamically adjust column widths based on the font size
        address_width = selected_size * 10  # Proportional width for Address column
        byte_width = selected_size * 3  # Proportional width for each byte column
        ascii_width = selected_size * 8  # Proportional width for ASCII column

        # Set column widths dynamically
        self.hex_table.setColumnWidth(0, address_width)  # Address column
        for i in range(1, 17):  # Set uniform width for each byte column
            self.hex_table.setColumnWidth(i, byte_width)
        self.hex_table.setColumnWidth(17, ascii_width)  # ASCII column

        # Update the font size for the headers as well
        header_font = self.hex_table.horizontalHeader().font()
        header_font.setPointSize(selected_size)
        self.hex_table.horizontalHeader().setFont(header_font)

        # Apply the font to all existing data cells
        for row in range(self.hex_table.rowCount()):
            for col in range(self.hex_table.columnCount()):
                item = self.hex_table.item(row, col)
                if item:
                    item_font = item.font()
                    # Ensure we have a valid font size before setting it
                    if selected_size > 0:
                        item_font.setPointSize(selected_size)
                        item.setFont(item_font)

        # Adjust the horizontal scrollbar policy if needed
        if self.hex_table.horizontalHeader().length() > self.hex_table.viewport().width():
            self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        else:
            self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def setup_hex_table(self):
        self.hex_table = QTableWidget()
        self.hex_table.verticalHeader().setDefaultSectionSize(20)  # Smaller row height

        # Set the font of the hex_table
        font = QFont("Courier")
        font.setPointSize(10)  # Default smaller font size
        font.setLetterSpacing(QFont.AbsoluteSpacing, 1)  # Reduce letter spacing
        self.hex_table.setFont(font)

        # Configure the columns and headers
        self.hex_table.setColumnCount(18)  # 16 bytes + 1 address + 1 ASCII
        self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])
        self.hex_table.verticalHeader().setVisible(False)

        # Set resizing policies for the header
        header = self.hex_table.horizontalHeader()
        header.setStyleSheet("QHeaderView::section { padding: 2px; }")  # Reduce header padding
        header.setDefaultSectionSize(25)  # Set a smaller default size

        # Address column - Resize based on content
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)

        # Byte columns - Fixed width for each byte column
        for i in range(1, 17):  # 00 to 0F columns
            header.setSectionResizeMode(i, QHeaderView.Fixed)
            self.hex_table.setColumnWidth(i, 25)  # Smaller byte column width

        # ASCII column - Stretch to fill remaining space
        header.setSectionResizeMode(17, QHeaderView.Stretch)

        # Adjust for remaining space in the ASCII column
        header.setStretchLastSection(True)

        # Set the initial column widths
        self.hex_table.setColumnWidth(0, 120)  # Address column initial width
        self.hex_table.setColumnWidth(17, 200)  # ASCII column initial width

        self.hex_table.setStyleSheet("""
            QTableWidget {
                gridline-color: transparent;
                border: 1px solid #d3d3d3;
            }
            QTableWidget::item {
                padding: 0px;
                border: none;
            }
        """)
        self.hex_table.setShowGrid(False)
        self.hex_table.setAlternatingRowColors(True)
        self.hex_table.setEditTriggers(QAbstractItemView.NoEditTriggers)

    def display_hex_content(self, file_content):
        hex_content = file_content.hex()
        self.search_results_widget.clear()
        # self.search_results_frame.setVisible(False)

        # Clear the search bar text
        self.search_bar.setText("")
        self.hex_viewer_manager = HexViewerManager(hex_content, file_content)
        self.update_navigation_states()
        self.display_current_page()
        # clear the page number entry
        self.page_entry.setText("")

    def export_content(self, selected_format):
        if not self.hex_viewer_manager:
            QMessageBox.warning(self, "No Content", "No content available to export.")
            return

        options = QFileDialog.Options()
        options |= QFileDialog.ReadOnly  # Allow read-only access to the selected file

        if selected_format == "txt":
            file_name, _ = QFileDialog.getSaveFileName(
                self, "Export Hex Content", "", "Text Files (*.txt)", options=options
            )
            self.export_as_text(file_name)
        elif selected_format == "html":
            file_name, _ = QFileDialog.getSaveFileName(
                self, "Export Hex Content", "", "HTML Files (*.html)", options=options
            )
            self.export_as_html(file_name)
        else:
            QMessageBox.warning(self, "Unsupported Format", "Unsupported export format selected.")

    def export_as_text(self, file_name):
        with open(file_name, "w") as text_file:
            # Add the header line with green color using ANSI escape codes
            header_line = "Address     00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F        ASCII"
            text_file.write(header_line + "\n")

            # Add an empty line
            text_file.write("\n")

            # Write the formatted hex content
            formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)
            text_file.write(formatted_hex)

    def export_as_html(self, file_name):
        html_content = "<html><body>\n"
        html_content += "<pre>\n"

        # Add a smaller and less prominent header with the original text
        header_line = '<div style="font-size:14px; color:#888;">Generated by Trace</div>'
        html_content += header_line + "<br><br>\n"

        # Add directory and file name information
        directory, filename = os.path.split(file_name)
        html_content += f'<span style="color:blue;">Directory: {directory}</span><br>\n'
        html_content += f'<span style="color:blue;">File Name: {filename}</span><br><br>\n'

        # Add the green header line
        header_line = ('<span style="color:green;">Address     00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F        '
                       'ASCII</span>')
        html_content += header_line + "<br>\n"

        html_content += self.hex_viewer_manager.format_hex(self.current_page).replace("\n", "<br>")
        html_content += "</pre>\n"
        html_content += "</body></html>"

        with open(file_name, "w") as html_file:
            html_file.write(html_content)

    def parse_hex_line(self, line):
        if ":" not in line:
            return None, None, None
        address, rest = line.split(":", maxsplit=1)
        hex_chunk, ascii_repr = rest.split("  ", maxsplit=1)
        return address.strip(), hex_chunk.strip(), ascii_repr.strip()

    def clear_content(self):
        self.hex_table.clear()

    def load_first_page(self):
        try:
            self.current_page = 0
            self.display_current_page()
        except (AttributeError, IndexError) as e:
            print(f"Error occurred: {e}")

    def load_last_page(self):
        try:
            self.current_page = self.hex_viewer_manager.total_pages() - 1
            self.display_current_page()
        except (AttributeError, IndexError) as e:
            print(f"Error occurred: {e}")

    def next_page(self):
        try:
            if self.current_page < self.hex_viewer_manager.total_pages() - 1:
                self.current_page += 1
            self.display_current_page()
        except (AttributeError, IndexError) as e:
            print(f"Error occurred: {e}")

    def previous_page(self):
        try:
            if self.current_page > 0:
                self.current_page -= 1
            self.display_current_page()
        except (AttributeError, IndexError) as e:
            print(f"Error occurred: {e}")

    def search_result_clicked(self, item):
        address = item.text().split(":")[1].strip()
        self.navigate_to_address(address)

    def display_current_page(self):
        formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)

        # Clear the table first
        self.hex_table.setRowCount(0)
        self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])

        hex_lines = formatted_hex.split('\n')

        # Ensure we set the correct row count
        self.hex_table.setRowCount(len(hex_lines))

        # Get the current font size with fallback to default
        current_font = self.hex_table.font()
        current_font_size = current_font.pointSize()
        if current_font_size <= 0:  # Use default if invalid
            current_font_size = 10  # Default font size

        for row, line in enumerate(hex_lines):
            address, hex_chunk, ascii_repr = self.parse_hex_line(line)
            if not address or not hex_chunk:  # Skip if there's an error in parsing
                continue

            # Set address and center-align
            address_item = QTableWidgetItem(address + ":")  # Add a colon after the address
            address_item.setTextAlignment(Qt.AlignCenter)
            item_font = address_item.font()
            item_font.setPointSize(current_font_size)
            address_item.setFont(item_font)
            self.hex_table.setItem(row, 0, address_item)

            # Set hex values and center-align
            for col, byte in enumerate(hex_chunk.split()):
                byte_item = QTableWidgetItem(byte)
                byte_item.setTextAlignment(Qt.AlignCenter)
                byte_item.setBackground(Qt.white)  # Clear any previous highlight
                item_font = byte_item.font()
                item_font.setPointSize(current_font_size)
                byte_item.setFont(item_font)
                self.hex_table.setItem(row, col + 1, byte_item)

            # Set ASCII representation and center-align
            ascii_item = QTableWidgetItem(ascii_repr)
            ascii_item.setTextAlignment(Qt.AlignCenter)
            item_font = ascii_item.font()
            item_font.setPointSize(current_font_size)
            ascii_item.setFont(item_font)
            self.hex_table.setItem(row, 17, ascii_item)

        self.update_navigation_states()

    def go_to_page_by_entry(self):
        try:
            page_num = int(self.page_entry.text()) - 1
            if 0 <= page_num < self.hex_viewer_manager.total_pages():
                self.current_page = page_num
                self.display_current_page()
                self.update_navigation_states()
            else:
                QMessageBox.warning(self, "Invalid Page", "Page number out of range.")
        except ValueError:
            QMessageBox.warning(self, "Invalid Page", "Please enter a valid page number.")

    def update_navigation_states(self):
        if not self.hex_viewer_manager:
            self.prev_action.setEnabled(False)
            self.next_action.setEnabled(False)
            return

        self.prev_action.setEnabled(self.current_page > 0)
        self.next_action.setEnabled(self.current_page < self.hex_viewer_manager.total_pages() - 1)
        self.page_entry.setText(str(self.current_page + 1))
        self.total_pages_label.setText(f"of {self.hex_viewer_manager.total_pages()}")

    def update_total_pages_label(self):
        total_pages = self.hex_viewer_manager.total_pages()
        current_page = self.current_page + 1
        self.total_pages_label.setText(f"{current_page} of {total_pages}")

    def trigger_search(self):
        query = self.search_bar.text()
        if not query:
            QMessageBox.warning(self, "Search Error", "Please enter a search query.")
            return

        # Check if a search is already ongoing. If so, stop it before starting a new one.
        if hasattr(self, 'search_thread') and self.search_thread.isRunning():
            self.search_thread.quit()
            self.search_thread.wait()

        # Start the search in a new thread
        self.search_thread = QThread()
        self.search_worker = SearchWorker(self.hex_viewer_manager, query)
        self.search_worker.moveToThread(self.search_thread)

        # Connect signals and slots
        self.search_worker.search_finished.connect(self.handle_search_results)
        self.search_thread.started.connect(self.search_worker.run)
        self.search_thread.finished.connect(self.cleanup_thread_resources)

        # Start the thread
        self.search_thread.start()

    def cleanup_thread_resources(self):
        # Ensure safe cleanup by checking the existence of resources before deleting
        if hasattr(self, 'search_worker'):
            self.search_worker.deleteLater()
            del self.search_worker
        if hasattr(self, 'search_thread'):
            self.search_thread.deleteLater()
            del self.search_thread

    def closeEvent(self, event):
        if hasattr(self, 'search_thread') and self.search_thread.isRunning():
            self.search_thread.quit()
            self.search_thread.wait()
        super().closeEvent(event)

    def handle_search_results(self, matches):
        self.search_results_widget.clear()  # Clear previous results
        if matches:
            for match in matches:
                address = f"0x{match * 16:08x}"  # Calculate the address from line number
                self.search_results_widget.addItem(f"Address: {address}")

            # Show the search results frame and resize the splitter to allocate more space to results
            self.search_results_frame.setVisible(True)
            self.splitter.setSizes([self.width() * 0.6, self.width() * 0.4])  # Adjust sizes dynamically

        else:
            QMessageBox.warning(self, "Search Result", "No matches found.")
            # Even if no matches are found, the search results frame will still be shown
            self.splitter.setSizes([self.width() * 0.75, self.width() * 0.25])

    def navigate_to_address(self, address):
        try:
            # Convert the address string back to an integer
            address_int = int(address, 16)

            # Determine the line number from the address
            line = address_int // 16

            # The rest of the logic remains the same
            self.current_page = line // self.hex_viewer_manager.LINES_PER_PAGE
            self.display_current_page()

            # Navigate to the specific row on that page and highlight it
            row_in_page = line % self.hex_viewer_manager.LINES_PER_PAGE
            self.hex_table.selectRow(row_in_page)
            for col in range(1, 17):
                item = self.hex_table.item(row_in_page, col)
                if item:
                    item.setBackground(Qt.yellow)
            self.update_navigation_states()
        except ValueError:
            QMessageBox.warning(self, "Navigation Error", "Invalid address.")


================================================
FILE: modules/mainwindow.py
================================================
import configparser
import hashlib
import os
import datetime
import pyewf
import pytsk3
import tempfile
import gc
import time
import logging
import re
from typing import Optional, Dict, Any, List, Tuple
from Registry import Registry
from sqlite3 import connect as sqlite3_connect
import subprocess
import platform
from contextlib import contextmanager
from functools import lru_cache
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QMargins
from PySide6.QtGui import QIcon, QFont, QPalette, QBrush, QAction, QActionGroup, QPixmap, QPainter, QColor
from PySide6.QtCharts import QChart, QChartView, QPieSeries, QPieSlice
from PySide6.QtWidgets import (QMainWindow, QMenuBar, QMenu, QToolBar, QDockWidget, QTreeWidget, QTabWidget,
                               QFileDialog, QTreeWidgetItem, QTableWidget, QMessageBox, QTableWidgetItem,
                               QDialog, QVBoxLayout, QHBoxLayout, QInputDialog, QDialogButtonBox, QHeaderView, QLabel, QLineEdit,
                               QFormLayout, QApplication, QWidget, QProgressDialog, QSizePolicy, QGroupBox,
                               QCheckBox, QGridLayout, QScrollArea, QPushButton, QToolButton)

from modules.about import AboutDialog
from modules.converter import Main
from modules.exif_tab import ExifViewer
from modules.file_carving import FileCarvingWidget
from modules.hex_tab import HexViewer
from modules.metadata_tab import MetadataViewer
from modules.registry import RegistryExtractor
from modules.text_tab import TextViewer
from modules.unified_application_manager import UnifiedViewer
from modules.verification import VerificationWidget
from modules.veriphone_api import VeriphoneWidget
from modules.virus_total_tab import VirusTotal

SECTOR_SIZE = 512
CHUNK_SIZE = 4 * 1024 * 1024  # 4MB chunks for processing
FILE_BUFFER_SIZE = 4096  # 4KB for file operations

# ==================== CONFIGURATION CONSTANTS ====================
# Logger setup
logger = logging.getLogger('TRACE.MainWindow')

# Window dimensions
DEFAULT_WINDOW_WIDTH = 1200
DEFAULT_WINDOW_HEIGHT = 800
DEFAULT_WINDOW_X = 100
DEFAULT_WINDOW_Y = 100

# Dock sizes
VIEWER_DOCK_MIN_HEIGHT = 222
VIEWER_DOCK_MAX_WIDTH = 1200
VIEWER_DOCK_MAX_SIZE = 16777215  # Qt maximum size value

# Column widths for listing table
COLUMN_WIDTHS = {
    'name': 400,        # Widest - file names can be long
    'inode': 50,        # Compact - numbers don't vary much
    'type': 50,         # Compact - short text like "File", "Dir"
    'size': 100,         # Compact - formatted sizes
    'created': 160,      # Narrower - timestamps are consistent length
    'accessed': 160,     # Narrower - timestamps are consistent length
    'modified': 160,     # Narrower - timestamps are consistent length
    'changed': 160,      # Narrower - timestamps are consistent length
    'path': 1100         # Wide - paths can be long
}

# Progress dialog settings
PROGRESS_DIALOG_WIDTH = 300

# Timeouts (in seconds)
MOUNT_TIMEOUT = 30
INFO_TIMEOUT = 10
PROCESS_TIMEOUT = 30
THREAD_SLEEP_MS = 1000  # milliseconds

# Minimum duration for progress dialog (milliseconds)
PROGRESS_MIN_DURATION = 1500

# Icon size
TREE_ICON_SIZE = 16
TABLE_ICON_SIZE = 24
TOOLBAR_ICON_SIZE = 16

# Table settings
TABLE_COLUMN_COUNT = 9
TABLE_BATCH_SIZE = 200  # Number of rows to process before updating UI

# Input field settings
INPUT_FIELD_MIN_WIDTH = 400
API_DIALOG_WIDTH = 600

# Qt maximum size constant
QT_MAX_SIZE = 16777215
# ================================================================


# Define a utility function for safe datetime conversion
def safe_datetime(timestamp):
    if timestamp is None or timestamp == 0:
        return "N/A"
    try:
        return datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + " UTC"
    except Exception:
        return "N/A"


# Utility class for common operations
class FileSystemUtils:
    @staticmethod
    def get_readable_size(size_in_bytes):
        """Convert bytes to a human-readable string (e.g., KB, MB, GB, TB)."""
        if size_in_bytes is None:
            return "0 B"

        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if size_in_bytes < 1024.0:
                return f"{size_in_bytes:.2f} {unit}"
            size_in_bytes /= 1024.0
        return f"{size_in_bytes:.2f} PB"

    @staticmethod
    @contextmanager
    def temp_file():
        """Context manager for temporary files, ensuring cleanup."""
        temp_path = None
        try:
            with tempfile.NamedTemporaryFile(delete=False) as temp:
                temp_path = temp.name
                yield temp_path
        finally:
            if temp_path and os.path.exists(temp_path):
                os.remove(temp_path)


# Class to handle EWF images
class EWFImgInfo(pytsk3.Img_Info):
    def __init__(self, ewf_handle):
        self._ewf_handle = ewf_handle
        super(EWFImgInfo, self).__init__(url="", type=pytsk3.TSK_IMG_TYPE_EXTERNAL)

    def close(self):
        self._ewf_handle.close()

    def read(self, offset, size):
        self._ewf_handle.seek(offset)
        return self._ewf_handle.read(size)

    def get_size(self):
        return self._ewf_handle.get_media_size()


# ImageHandler class with optimizations
class ImageHandler:
    def __init__(self, image_path):
        self.image_path = image_path
        self.img_info = None
        self.volume_info = None
        self.fs_info_cache = {}
        self.fs_info = None
        self.is_wiped_image = False
        self._directory_cache = {}  # Cache for directory contents
        self._partition_cache = None  # Cache for partitions

        # Load the image with progress tracking
        self.load_image()

    def __del__(self):
        """Cleanup resources when the object is destroyed."""
        self.close_resources()

    def close_resources(self):
        """Explicitly close all open resources."""
        # Close filesystem objects
        for fs_info in self.fs_info_cache.values():
            if hasattr(fs_info, 'close'):
                try:
                    fs_info.close()
                except:
                    pass

        # Close the image
        if self.img_info:
            if hasattr(self.img_info, 'close'):
                try:
                    self.img_info.close()
                except:
                    pass
            self.img_info = None

        # Clear caches
        self.fs_info_cache.clear()
        self._directory_cache.clear()

    def get_size(self):
        """Returns the size of the disk image."""
        if self.img_info:
            return self.img_info.get_size()
        else:
            raise AttributeError("Image not loaded or unsupported format.")

    def read(self, offset, size):
        """Reads data from the image starting at `offset` for `size` bytes."""
        if self.img_info and hasattr(self.img_info, 'read'):
            return self.img_info.read(offset, size)
        else:
            raise NotImplementedError("The image format does not support direct reading.")

    def build_allocation_map(self, start_offset):
        """Build a map of allocated disk regions by traversing the filesystem."""
        allocation_map = []

        try:
            fs_info = self.get_fs_info(start_offset)
            if not fs_info:
                logger.warning(f"Unable to get filesystem info for offset {start_offset}")
                return allocation_map

            # Get block size for this filesystem
            block_size = fs_info.info.block_size

            # Recursively walk filesystem to find all allocated files
            def walk_directory(directory, path="/"):
                """Recursively walk directory and collect allocated file ranges."""
                try:
                    for entry in directory:
                        # Skip current and parent directory entries
                        if not hasattr(entry, 'info') or not hasattr(entry.info, 'name'):
                            continue

                        name = entry.info.name.name.decode('utf-8', errors='ignore')
                        if name in [".", ".."]:
                            continue

                        # Check if this is an allocated file
                        if not hasattr(entry.info, 'meta') or entry.info.meta is None:
                            continue

                        # Only process allocated files (skip deleted files)
                        is_allocated = bool(int(entry.info.meta.flags) & pytsk3.TSK_FS_META_FLAG_ALLOC)
                        if not is_allocated:
                            continue

                        # Get file size and inode
                        file_size = entry.info.meta.size

                        # Only process files with actual data
                        if file_size > 0:
                            try:
                                # Open the file to access its data runs
                                file_obj = fs_info.open_meta(inode=entry.info.meta.addr)

                                # Calculate byte offsets for the file's data
                                # This is approximate - we use the file's logical position
                                # For a more accurate map, we'd need to walk data runs
                                # but this is a reasonable approximation for most filesystems

                                # Get partition offset in bytes
                                partition_offset_bytes = start_offset * 512

                                # For simplicity, we'll mark regions based on inode metadata
                                # A more sophisticated approach would walk TSK_FS_BLOCK structures
                                # but pytsk3 doesn't expose block_walk easily

                                # Estimate file location based on inode number and size
                                # This is a simplified approach - actual blocks may be fragmented
                                inode_addr = entry.info.meta.addr
                                estimated_start = partition_offset_bytes + (inode_addr * block_size)
                                estimated_end = estimated_start + file_size

                                allocation_map.append((estimated_start, estimated_end))

                            except Exception as e:
                                # Skip files we can't open
                                logger.debug(f"Could not process file {path}{name}: {e}")
                                pass

                        # Recursively process directories
                        if entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
                            try:
                                sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)
                                walk_directory(sub_directory, f"{path}{name}/")
                            except Exception as e:
                                logger.debug(f"Could not open directory {path}{name}: {e}")
                                pass

                except Exception as e:
                    logger.debug(f"Error walking directory {path}: {e}")
                    pass

            # Start walking from root directory
            try:
                root_dir = fs_info.open_dir(path="/")
                walk_directory(root_dir)
            except Exception as e:
                logger.error(f"Error accessing root directory: {e}")

            # Sort allocation map by start offset for efficient searching
            allocation_map.sort(key=lambda x: x[0])

            logger.info(f"Built allocation map with {len(allocation_map)} allocated file regions")

        except Exception as e:
            logger.error(f"Error building allocation map: {e}")

        return allocation_map

    def get_image_type(self):
        """Determine the type of the image based on its extension."""
        _, extension = os.path.splitext(self.image_path)
        extension = extension.lower()

        ewf = [".e01", ".s01", ".l01", ".ex01"]
        raw = [".raw", ".img", ".dd", ".iso",
               ".ad1", ".001", ".dmg", ".sparse",
               ".sparseimage"]

        if extension in ewf:
            return "ewf"
        elif extension in raw:
            return "raw"
        else:
            raise ValueError(f"Unsupported image type: {extension}")

    def calculate_hashes(self, progress_callback=None):
        """Calculate the MD5, SHA1, and SHA256 hashes for the image with progress reporting."""
        hash_md5 = hashlib.md5()
        hash_sha1 = hashlib.sha1()
        hash_sha256 = hashlib.sha256()
        size = 0
        total_size = 0
        stored_md5, stored_sha1 = None, None

        image_type = self.get_image_type()

        try:
            # First get total size for progress reporting
            if image_type == "ewf":
                filenames = pyewf.glob(self.image_path)
                ewf_handle = pyewf.handle()
                try:
                    ewf_handle.open(filenames)
                    total_size = ewf_handle.get_media_size()

                    try:
                        # Attempt to retrieve the stored hash values
                        stored_md5 = ewf_handle.get_hash_value("MD5")
                        stored_sha1 = ewf_handle.get_hash_value("SHA1")
                    except Exception as e:
                        logger.warning(f"Unable to retrieve stored hash values: {e}")

                    # Calculate hashes in chunks
                    while True:
                        chunk = ewf_handle.read(CHUNK_SIZE)
                        if not chunk:
                            break

                        hash_md5.update(chunk)
                        hash_sha1.update(chunk)
                        hash_sha256.update(chunk)
                        size += len(chunk)

                        # Report progress safely
                        if progress_callback and total_size > 0:
                            try:
                                progress_callback(size, total_size)
                            except Exception as e:
                                logger.error(f"Progress callback error: {e}")
                finally:
                    ewf_handle.close()

            elif image_type == "raw":
                try:
                    total_size = os.path.getsize(self.image_path)
                    with open(self.image_path, "rb") as f:
                        while True:
                            chunk = f.read(CHUNK_SIZE)
                            if not chunk:
                                break

                            hash_md5.update(chunk)
                            hash_sha1.update(chunk)
                            hash_sha256.update(chunk)
                            size += len(chunk)

                            # Report progress safely
                            if progress_callback and total_size > 0:
                                try:
                                    progress_callback(size, total_size)
                                except Exception as e:
                                    logger.error(f"Progress callback error: {e}")
                except Exception as e:
                    logger.error(f"Error reading raw image: {e}")

            # Compile the computed and stored hashes in a dictionary
            hashes = {
                'computed_md5': hash_md5.hexdigest(),
                'computed_sha1': hash_sha1.hexdigest(),
                'computed_sha256': hash_sha256.hexdigest(),
                'size': size,
                'path': self.image_path,
                'stored_md5': stored_md5,
                'stored_sha1': stored_sha1
            }

            return hashes
        except Exception as e:
            logger.error(f"Error calculating hashes: {e}")
            return {
                'computed_md5': 'Error',
                'computed_sha1': 'Error',
                'computed_sha256': 'Error',
                'size': 0,
                'path': self.image_path,
                'stored_md5': None,
                'stored_sha1': None,
                'error': str(e)
            }

    def load_image(self):
        """Load the image and retrieve volume and filesystem information."""
        image_type = self.get_image_type()

        try:
            if image_type == "ewf":
                filenames = pyewf.glob(self.image_path)
                ewf_handle = pyewf.handle()
                ewf_handle.open(filenames)
                self.img_info = EWFImgInfo(ewf_handle)
            elif image_type == "raw":
                self.img_info = pytsk3.Img_Info(self.image_path)
            else:
                raise ValueError(f"Unsupported image type: {image_type}")

            try:
                self.volume_info = pytsk3.Volume_Info(self.img_info)
            except Exception:
                self.volume_info = None
                # Attempt to detect a filesystem directly if no volume info
                try:
                    self.fs_info = pytsk3.FS_Info(self.img_info)
                except Exception:
                    self.fs_info = None
                    # If no volume info and no filesystem, mark as wiped
                    self.is_wiped_image = True
        except Exception as e:
            logger.error(f"Error loading image: {e}")
            self.img_info = None
            self.volume_info = None
            self.fs_info = None
            self.is_wiped_image = True

    def has_filesystem(self, start_offset):
        fs_info = self.get_fs_info(start_offset)
        return fs_info is not None

    def is_wiped(self):
        # Image is considered wiped if no volume info, no filesystem detected
        return self.is_wiped_image

    @property
    def partitions(self):
        """Get partitions with caching."""
        if self._partition_cache is None:
            self._partition_cache = self._get_partitions()
        return self._partition_cache

    def get_partitions(self):
        """Retrieve partitions from the loaded image, or indicate unpartitioned space."""
        return self.partitions

    def _get_partitions(self):
        """Internal method to actually retrieve partitions."""
        partitions = []
        if self.volume_info:
            for partition in self.volume_info:
                if not partition.desc:
                    continue
                partitions.append((partition.addr, partition.desc, partition.start, partition.len))
        return partitions

    @lru_cache(maxsize=32)
    def get_fs_info(self, start_offset):
        """Retrieve the FS_Info for a partition, initializing it if necessary."""
        if start_offset not in self.fs_info_cache:
            try:
                fs_info = pytsk3.FS_Info(self.img_info, offset=start_offset * 512)
                self.fs_info_cache[start_offset] = fs_info
            except Exception as e:
                return None
        return self.fs_info_cache[start_offset]

    @lru_cache(maxsize=32)
    def get_fs_type(self, start_offset):
        """Retrieve the file system type for a partition."""
        try:
            fs_type = self.get_fs_info(start_offset).info.ftype

            # Map the file system type to its name
            fs_type_map = {
                pytsk3.TSK_FS_TYPE_NTFS: "NTFS",
                pytsk3.TSK_FS_TYPE_FAT12: "FAT12",
                pytsk3.TSK_FS_TYPE_FAT16: "FAT16",
                pytsk3.TSK_FS_TYPE_FAT32: "FAT32",
                pytsk3.TSK_FS_TYPE_EXFAT: "ExFAT",
                pytsk3.TSK_FS_TYPE_EXT2: "Ext2",
                pytsk3.TSK_FS_TYPE_EXT3: "Ext3",
                pytsk3.TSK_FS_TYPE_EXT4: "Ext4",
                pytsk3.TSK_FS_TYPE_ISO9660: "ISO9660",
                pytsk3.TSK_FS_TYPE_HFS: "HFS",
                pytsk3.TSK_FS_TYPE_APFS: "APFS"
            }

            return fs_type_map.get(fs_type, "Unknown")
        except Exception as e:
            return "N/A"

    def check_partition_contents(self, partition_start_offset):
        """Check if a partition has any files or folders."""
        fs = self.get_fs_info(partition_start_offset)
        if fs:
            try:
                root_dir = fs.open_dir(path="/")
                for _ in root_dir:
                    return True
                return False
            except:
                return False
        return False

    def get_directory_contents(self, start_offset, inode_number=None):
        """Get directory contents with caching for performance."""
        cache_key = f"{start_offset}_{inode_number}"

        # Check if we have this directory in our cache
        if cache_key in self._directory_cache:
            return self._directory_cache[cache_key]

        fs = self.get_fs_info(start_offset)
        if fs:
            try:
                directory = fs.open_dir(inode=inode_number) if inode_number else fs.open_dir(path="/")
                entries = []

                for entry in directory:
                    if entry.info.name.name in [b".", b".."]:
                        continue

                    is_directory = False
                    if entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
                        is_directory = True

                    entries.append({
                        "name": entry.info.name.name.decode('utf-8', errors='replace') if hasattr(entry.info.name,
                                                                                                  'name') else None,
                        "is_directory": is_directory,
                        "inode_number": entry.info.meta.addr if entry.info.meta else None,
                        "size": entry.info.meta.size if entry.info.meta and entry.info.meta.size is not None else 0,
                        "accessed": safe_datetime(entry.info.meta.atime) if hasattr(entry.info.meta,
                                                                                    'atime') else "N/A",
                        "modified": safe_datetime(entry.info.meta.mtime) if hasattr(entry.info.meta,
                                                                                    'mtime') else "N/A",
                        "created": safe_datetime(entry.info.meta.crtime) if hasattr(entry.info.meta,
                                                                                    'crtime') else "N/A",
                        "changed": safe_datetime(entry.info.meta.ctime) if hasattr(entry.info.meta, 'ctime') else "N/A",
                    })

                # Cache results
                self._directory_cache[cache_key] = entries
                return entries

            except Exception as e:
                # Log the exception for debugging purposes
                logger.error(f"Error in get_directory_contents: {e}")
                return []
        return []

    def get_registry_hive(self, fs_info, hive_path):
        """Extract a registry hive from the given filesystem."""
        try:
            registry_file = fs_info.open(hive_path)
            hive_data = registry_file.read_random(0, registry_file.info.meta.size)
            return hive_data
        except Exception as e:
            logger.error(f"Error reading registry hive: {e}")
            return None

    def get_windows_version(self, start_offset):
        """Get the Windows version from the SOFTWARE registry hive."""
        fs_info = self.get_fs_info(start_offset)
        if not fs_info:
            return None

        # if file system is not ntfs, return unknown OS and exit the function
        if self.get_fs_type(start_offset) != "NTFS":
            return None

        software_hive_data = self.get_registry_hive(fs_info, "/Windows/System32/config/SOFTWARE")

        if not software_hive_data:
            return None

        # Use a context manager to handle the temporary file
        with FileSystemUtils.temp_file() as temp_hive_path:
            try:
                with open(temp_hive_path, 'wb') as temp_hive:
                    temp_hive.write(software_hive_data)

                reg = Registry.Registry(temp_hive_path)
                key = reg.open("Microsoft\\Windows NT\\CurrentVersion")

                # Helper function to safely get registry values
                def get_reg_value(reg_key, value_name):
                    try:
                        return reg_key.value(value_name).value()
                    except Registry.RegistryValueNotFoundException:
                        return "N/A"

                # Fetching registry values
                product_name = get_reg_value(key, "ProductName")
                current_version = get_reg_value(key, "CurrentVersion")
                current_build = get_reg_value(key, "CurrentBuild")
                registered_owner = get_reg_value(key, "RegisteredOwner")
                csd_version = get_reg_value(key, "CSDVersion")
                product_id = get_reg_value(key, "ProductId")

                return f"{product_name} Version {current_version}\nBuild {current_build} {csd_version}\nOwner: {registered_owner}\nProduct ID: {product_id}"

            except Exception as e:
                logger.error(f"Error parsing SOFTWARE hive: {e}")
                return "Error in parsing OS version"

    def read_unallocated_space(self, start_offset, end_offset):
        try:
            start_byte_offset = start_offset * SECTOR_SIZE
            end_byte_offset = max(end_offset * SECTOR_SIZE, start_byte_offset + SECTOR_SIZE - 1)
            size_in_bytes = end_byte_offset - start_byte_offset + 1  # Ensuring at least some data is read

            if size_in_bytes <= 0:
                logger.warning("Invalid size for unallocated space, adjusting to read at least one sector.")
                size_in_bytes = SECTOR_SIZE  # Adjust to read at least one sector

            # For large blocks, read in chunks instead of all at once
            if size_in_bytes > CHUNK_SIZE:
                chunks = []
                for offset in range(start_byte_offset, end_byte_offset, CHUNK_SIZE):
                    remaining = min(CHUNK_SIZE, end_byte_offset - offset + 1)
                    chunk = self.img_info.read(offset, remaining)
                    if not chunk:
                        break
                    chunks.append(chunk)

                if not chunks:
                    return None

                return b''.join(chunks)
            else:
                unallocated_space = self.img_info.read(start_byte_offset, size_in_bytes)
                if unallocated_space is None or len(unallocated_space) == 0:
                    logger.error(f"Failed to read unallocated space from offset {start_byte_offset} to {end_byte_offset}")
                    return None
                return unallocated_space

        except Exception as e:
            logger.error(f"Error reading unallocated space: {e}")
            return None

    def open_image(self):
        if self.get_image_type() == "ewf":
            filenames = pyewf.glob(self.image_path)
            ewf_handle = pyewf.handle()
            ewf_handle.open(filenames)
            return EWFImgInfo(ewf_handle)
        else:
            return pytsk3.Img_Info(self.image_path)

    def list_files(self, extensions=None):
        """Get a list of all files with given extensions."""
        files_list = []
        img_info = self.open_image()

        try:
            volume_info = pytsk3.Volume_Info(img_info)
            for partition in volume_info:
                if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:
                    # Store offset in SECTORS (not bytes)
                    self.process_partition(img_info, partition.start, files_list, extensions)
        except IOError:
            self.process_partition(img_info, 0, files_list, extensions)

        return files_list

    def process_partition(self, img_info, offset_sectors, files_list, extensions):
        """Process partition listing - offset_sectors is in sectors, not bytes."""
        try:
            fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)
            self._recursive_file_search(fs_info, fs_info.open_dir(path="/"), "/", files_list, extensions, None, offset_sectors)
        except IOError as e:
            logger.error(f"Unable to open filesystem at offset {offset_sectors}: {e}")

    def _recursive_file_search(self, fs_info, directory, parent_path, files_list, extensions, search_query=None, start_offset=0):
        """Recursively search for files in a directory."""
        for entry in directory:
            if entry.info.name.name in [b".", b".."]:
                continue

            try:
                file_name = entry.info.name.name.decode("utf-8", errors='replace')
                file_extension = os.path.splitext(file_name)[1].lower()

                # Determine if this entry should be included in results
                is_directory = entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR

                if search_query:
                    # If there's a search query, check if the file name contains the query
                    if search_query.startswith('.'):
                        # If the search query is an extension (e.g., '.jpg')
                        query_matches = file_extension == search_query.lower()
                        match_reason = f"extension matches '{search_query}'" if query_matches else ""
                    else:
                        # If the search query is a file name or part of it (SUBSTRING MATCH)
                        query_matches = search_query.lower() in file_name.lower()
                        match_reason = f"filename contains '{search_query}'" if query_matches else ""
                else:
                    # If no search query, handle based on extensions
                    if is_directory:
                        # Always include directories when no search query (for navigation)
                        query_matches = True
                        match_reason = "directory (no filter)"
                    else:
                        # For files, apply extension filter
                        query_matches = extensions is None or file_extension in extensions or '' in extensions
                        match_reason = "extension filter"

                if is_directory:
                    # If directory matches search query, add it to results
                    if query_matches:
                        dir_info = self._get_directory_metadata(entry, parent_path, start_offset)
                        files_list.append(dir_info)
                        if logger.isEnabledFor(logging.DEBUG):
                            logger.debug(f"MATCH (DIR): '{file_name}' - {match_reason}")

                    # Recursively search subdirectory
                    try:
                        sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)
                        self._recursive_file_search(fs_info, sub_directory, os.path.join(parent_path, file_name),
                                                    files_list,
                                                    extensions, search_query, start_offset)
                    except IOError as e:
                        logger.error(f"Unable to open directory: {e}")

                elif entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_REG and query_matches:
                    file_info = self._get_file_metadata(entry, parent_path, start_offset)
                    files_list.append(file_info)
                    if logger.isEnabledFor(logging.DEBUG):
                        logger.debug(f"MATCH (FILE): '{file_name}' - {match_reason}")
            except UnicodeDecodeError:
                continue  # Skip entries with encoding issues

    def _get_directory_metadata(self, entry, parent_path, start_offset=0):
        """Get directory metadata for search results."""
        try:
            dir_name = entry.info.name.name.decode("utf-8", errors='replace')
            inode_number = entry.info.meta.addr if entry.info.meta else 0

            # Get volume name for this offset
            volume_name = self._get_volume_name_for_offset(start_offset)
            # Create full path with volume information
            full_path = f"{volume_name}:{os.path.join(parent_path, dir_name)}"

            return {
                "name": dir_name,
                "path": full_path,
                "size": 0,  # Directories don't have a size in this context
                "accessed": safe_datetime(entry.info.meta.atime if entry.info.meta else None),
                "modified": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),
                "created": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),
                "changed": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),
                "inode_item": str(inode_number),
                "inode_number": inode_number,
                "start_offset": start_offset,
                "is_directory": True,  # Mark as directory
                "type": "directory"
            }
        except Exception as e:
            logger.error(f"Error getting directory metadata: {e}")
            return {
                "name": "Error reading directory",
                "path": parent_path + "/unknown",
                "size": 0,
                "accessed": "N/A",
                "modified": "N/A",
                "created": "N/A",
                "changed": "N/A",
                "inode_item": "0",
                "inode_number": 0,
                "start_offset": start_offset,
                "is_directory": True,
                "type": "directory"
            }

    def _get_volume_name_for_offset(self, start_offset):
        """Get the volume name (e.g., 'vol0', 'vol1') for a given partition offset."""
        try:
            partitions = self.get_partitions()
            for addr, desc, start, length in partitions:
                if start == start_offset:
                    return f"vol{addr}"
            # If not found in partitions, it might be a single filesystem image
            return "vol0"
        except Exception as e:
            logger.warning(f"Could not determine volume name for offset {start_offset}: {e}")
            return "vol0"

    def _get_file_metadata(self, entry, parent_path, start_offset=0):
        """Get file metadata including all fields needed for viewing."""
        try:
            file_name = entry.info.name.name.decode("utf-8", errors='replace')
            inode_number = entry.info.meta.addr if entry.info.meta else 0

            # Get volume name for this offset
            volume_name = self._get_volume_name_for_offset(start_offset)
            # Create full path with volume information
            full_path = f"{volume_name}:{os.path.join(parent_path, file_name)}"

            return {
                "name": file_name,
                "path": full_path,  # Now includes volume information
                "size": entry.info.meta.size if entry.info.meta else 0,
                "accessed": safe_datetime(entry.info.meta.atime if entry.info.meta else None),
                "modified": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),
                "created": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),
                "changed": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),
                "inode_item": str(inode_number),  # For display compatibility
                "inode_number": inode_number,  # For file content retrieval
                "start_offset": start_offset,  # Partition offset needed for retrieval
                "is_directory": False,  # This method only called for files
                "type": "file"  # For compatibility with viewer logic
            }
        except Exception as e:
            logger.error(f"Error getting file metadata: {e}")
            # Return basic info when we encounter errors
            return {
                "name": "Error reading file",
                "path": parent_path + "/unknown",
                "size": 0,
                "accessed": "N/A",
                "modified": "N/A",
                "created": "N/A",
                "changed": "N/A",
                "inode_item": "0",
                "inode_number": 0,
                "start_offset": start_offset,
                "is_directory": False,
                "type": "file"
            }

    def search_files(self, search_query=None):
        logger.info(f"ImageHandler.search_files called with query: '{search_query}'")
        files_list = []
        img_info = self.open_image()

        try:
            volume_info = pytsk3.Volume_Info(img_info)
            partition_count = 0
            for partition in volume_info:
                if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:
                    partition_count += 1
                    logger.info(f"Searching partition {partition_count} (offset: {partition.start} sectors)")
                    # Store offset in SECTORS (not bytes) - get_fs_info will multiply by 512
                    self.process_partition_search(img_info, partition.start, files_list, search_query)
            logger.info(f"Searched {partition_count} allocated partitions")
        except IOError as e:
            # No volume information, attempt to read as a single filesystem
            logger.info(f"No volume info, reading as single filesystem: {e}")
            self.process_partition_search(img_info, 0, files_list, search_query)

        logger.info(f"Total files found: {len(files_list)}")
        return files_list

    def process_partition_search(self, img_info, offset_sectors, files_list, search_query):
        """Process partition search - offset_sectors is in sectors, not bytes."""
        try:
            logger.info(f"Opening filesystem at offset {offset_sectors} sectors ({offset_sectors * SECTOR_SIZE} bytes)")
            fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)
            logger.info(f"Starting recursive search with query: '{search_query}'")
            initial_count = len(files_list)
            self._recursive_file_search(fs_info, fs_info.open_dir(path="/"), "/", files_list, None, search_query, offset_sectors)
            logger.info(f"Recursive search complete. Found {len(files_list) - initial_count} files in this partition")
        except IOError as e:
            logger.error(f"Unable to open file system for search: {e}")

    def get_file_content(self, inode_number, offset):
        fs = self.get_fs_info(offset)
        if not fs:
            return None, None

        try:
            file_obj = fs.open_meta(inode=inode_number)
            if file_obj.info.meta.size == 0:
                logger.info("File has no content or is a special metafile!")
                return None, None

            # For large files, read in chunks
            file_size = file_obj.info.meta.size
            if file_size > CHUNK_SIZE:
                chunks = []
                for chunk_offset in range(0, file_size, CHUNK_SIZE):
                    chunk_size = min(CHUNK_SIZE, file_size - chunk_offset)
                    chunk = file_obj.read_random(chunk_offset, chunk_size)
                    if not chunk:
                        break
                    chunks.append(chunk)
                content = b''.join(chunks)
            else:
                # Small file, read all at once
                content = file_obj.read_random(0, file_size)

            metadata = file_obj.info.meta  # Collect the metadata
            return content, metadata

        except Exception as e:
            logger.error(f"Error reading file: {e}")
            return None, None

    # Replace static method assignment with an actual instance method
    def get_readable_size(self, size_in_bytes):
        """Convert bytes to a human-readable string, wrapper for the static utility method."""
        return FileSystemUtils.get_readable_size(size_in_bytes)


# DatabaseManager class with optimization
class DatabaseManager:
    def __init__(self, db_path):
        self.db_path = db_path
        self.db_conn = None
        self._icon_cache = {}  # Cache for icon paths
        self._connect()

    def _connect(self):
        """Establish a connection to the database with proper error handling."""
        try:
            self.db_conn = sqlite3_connect(self.db_path)
            # Enable foreign keys
            self.db_conn.execute("PRAGMA foreign_keys = ON")
        except Exception as e:
            logger.error(f"Error connecting to database: {e}")
            self.db_conn = None

    def __del__(self):
        """Ensure connection is closed when object is destroyed."""
        self.close()

    def close(self):
        """Explicitly close the database connection."""
        if self.db_conn:
            try:
                self.db_conn.close()
                self.db_conn = None
            except Exception as e:
                logger.error(f"Error closing database connection: {e}")

    def get_icon_path(self, icon_type, identifier):
        """Get icon path with caching for performance."""
        # Check cache first
        cache_key = f"{icon_type}_{identifier}"
        if cache_key in self._icon_cache:
            return self._icon_cache[cache_key]

        if not self.db_conn:
            self._connect()
            if not self.db_conn:
                return 'Icons/mimetypes/application-x-zerosize.svg'

        try:
            c = self.db_conn.cursor()
            # First, try to get the icon for the specific identifier
            c.execute("SELECT path FROM icons WHERE type = ? AND extention = ?", (icon_type, identifier))
            result = c.fetchone()

            # If a specific icon exists for the identifier, cache and return it
            if result:
                self._icon_cache[cache_key] = result[0]
                return result[0]

            # If no specific icon exists, check for default icons
            if icon_type == 'folder':
                c.execute("SELECT path FROM icons WHERE type = ? AND extention = 'folder'", (icon_type,))
                result = c.fetchone()
                default_path = result[0] if result else 'Icons/mimetypes/application-x-zerosize.svg'
            else:
                # Try to find a generic icon for the file type first
                generic_key = f"{icon_type}_generic"
                if generic_key not in self._icon_cache:
                    c.execute("SELECT path FROM icons WHERE type = ? AND extention = 'generic'", (icon_type,))
                    result = c.fetchone()
                    self._icon_cache[generic_key] = result[
                        0] if result else 'Icons/mimetypes/application-x-zerosize.svg'

                default_path = self._icon_cache[generic_key]

            # Cache the result before returning
            self._icon_cache[cache_key] = default_path
            return default_path

        except Exception as e:
            logger.error(f"Error fetching icon: {e}")
            return 'Icons/mimetypes/application-x-zerosize.svg'
        finally:
            if 'c' in locals():
                c.close()


# ImageManager class with optimizations
class ImageManager(QThread):
    operationCompleted = Signal(bool, str)  # Signal to indicate operation completion
    showMessage = Signal(str, str)  # Signal to show a message (Title, Content)
    progressUpdated = Signal(int)  # Signal for progress updates

    def __init__(self):
        super().__init__()
        self.operation = None
        self.image_path = None
        self.file_name = None
        self.is_running = False
        self._process = None

    def __del__(self):
        self.cleanup_resources()

    def cleanup_resources(self):
        """Clean up any resources used by the image mounting process."""
        if self._process and hasattr(self._process, 'poll') and self._process.poll() is None:
            try:
                self._process.terminate()
                self._process = None
            except:
                pass

    def run(self):
        self.is_running = True
        system = platform.system()

        try:
            if self.operation == 'mount' and self.image_path:
                if system == 'Darwin':  # macOS
                    self._mount_image_macos()
                elif system == 'Linux':  # Linux (including Kali)
                    self._mount_image_linux()
                elif system == 'Windows':  # Windows
                    self._mount_image_windows()
                else:
                    raise Exception("Unsupported Operating System")
            elif self.operation == 'dismount':
                if system == 'Darwin':
                    self._dismount_image_macos()
                elif system == 'Linux':
                    self._dismount_image_linux()
                elif system == 'Windows':
                    self._dismount_image_windows()
                else:
                    raise Exception("Unsupported Operating System")
        except Exception as e:
            self.operationCompleted.emit(False, f"Failed to {self.operation} the image. Error: {e}")
        finally:
            self.is_running = False

    def _mount_image_windows(self):
        """Mount image on Windows using Arsenal Image Mounter."""
        try:
            aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'
            if not os.path.exists(aim_path):
                self.operationCompleted.emit(False, "Arsenal Image Mounter not found. Please install it.")
                return

            cmd = [
                aim_path,
                '--mount',
                '--readonly',
                f'--filename={self.image_path}'
            ]

            # Use subprocess.Popen with proper parameter checking
            self._process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
            )

            # Wait for the process to complete or timeout after 30 seconds
            try:
                stdout, stderr = self._process.communicate(timeout=MOUNT_TIMEOUT)
                if self._process.returncode != 0:
                    error_msg = stderr.decode('utf-8', errors='replace')
                    self.operationCompleted.emit(False, f"Failed to mount the image: {error_msg}")
                    return
                self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully.")
            except subprocess.TimeoutExpired:
                # Process is taking too long, but this is sometimes normal for mounting
                # We'll assume it's working in the background
                self.operationCompleted.emit(True,
                                             f"Image {self.file_name} mount initiated. Check Windows Disk Management.")

        except Exception as e:
            self.operationCompleted.emit(False, f"Failed to mount the image on Windows. Error: {e}")

    def _mount_image_macos(self):
        """Mount image on macOS using hdiutil."""
        try:
            # Step 1: Attach the image without mounting it
            attach_cmd = [
                'hdiutil', 'attach',
                '-imagekey', 'diskimage-class=CRawDiskImage',
                '-nomount', self.image_path
            ]

            attach_process = subprocess.Popen(
                attach_cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT
            )

            # Wait with timeout
            try:
                attach_output, _ = attach_process.communicate(timeout=MOUNT_TIMEOUT)
                if attach_process.returncode != 0:
                    self.operationCompleted.emit(False, f"Failed to attach image: {attach_output.decode()}")
                    return
            except subprocess.TimeoutExpired:
                attach_process.kill()
                self.operationCompleted.emit(False, "Attaching image timed out")
                return

            attach_output = attach_output.decode().strip()

            # Step 2: Add a short delay to ensure the system has time to process the attachment
            QThread.msleep(THREAD_SLEEP_MS)  # More reliable than time.sleep in a QThread

            # Step 3: Extract the disk identifier from the output
            lines = attach_output.splitlines()
            disk_identifier = None

            for line in lines:
                if line.startswith('/dev/disk'):
                    disk_identifier = line.split()[0]
                    break

            if not disk_identifier:
                self.operationCompleted.emit(False, "Failed to find disk identifier after attaching the image.")
                return

            # Step 4: Mount the disk using the identifier
            mount_cmd = ['hdiutil', 'mount', disk_identifier]
            mount_process = subprocess.Popen(
                mount_cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT
            )

            try:
                mount_output, _ = mount_process.communicate(timeout=MOUNT_TIMEOUT)
                if mount_process.returncode != 0:
                    self.operationCompleted.emit(False, f"Failed to mount disk: {mount_output.decode()}")
                    return
            except subprocess.TimeoutExpired:
                mount_process.kill()
                self.operationCompleted.emit(False, "Mounting timed out")
                return

            mount_output = mount_output.decode().strip()

            # Step 5: Extract the mount point (e.g., /Volumes/LABEL2)
            lines = mount_output.splitlines()
            mount_point = None

            for line in lines:
                if line.startswith('/dev/') and '\t' in line:
                    mount_point = line.split('\t')[1]
                    break

            if mount_point:
                # Emit success with the mount point
                self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully at {mount_point}.")
            else:
                self.operationCompleted.emit(False, f"Image {self.file_name} mounted, but no volumes were detected.")

        except subprocess.CalledProcessError as e:
            self.operationCompleted.emit(False, f"Failed to mount the image on macOS. Error: {e.output.decode()}")
        except Exception as e:
            self.operationCompleted.emit(False, f"Unexpected error mounting image: {str(e)}")

    def _mount_image_linux(self):
        """Mount image on Linux using appropriate tools."""
        try:
            if self.image_path.lower().endswith('.e01'):
                # Use ewfmount for .e01 images
                ewf_mount_dir = '/mnt/ewf'

                # Create mount directory if it doesn't exist
                if not os.path.exists(ewf_mount_dir):
                    os.makedirs(ewf_mount_dir, exist_ok=True)

                # Run ewfmount with proper error handling
                ewf_cmd = ['sudo', 'ewfmount', self.image_path, ewf_mount_dir]
                ewf_process = subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)

                # Get the partition table info using fdisk
                fdisk_cmd = ['fdisk', '-l', os.path.join(ewf_mount_dir, 'ewf1')]
                fdisk_output = subprocess.check_output(fdisk_cmd, text=True)

                # Find the partition start sector
                partition_start_sector = None
                for line in fdisk_output.splitlines():
                    if '/dev/' in line and not line.startswith('Disk '):
                        # Assuming you want the first partition listed
                        parts = line.split()
                        if len(parts) > 1:
                            try:
                                partition_start_sector = int(parts[1])
                                break
                            except (ValueError, IndexError):
                                continue

                if partition_start_sector is None:
                    raise Exception("Failed to find partition start sector in the EWF image.")

                # Calculate the byte offset
                byte_offset = partition_start_sector * 512

                # Mount the partition using the calculated offset
                mount_dir = '/mnt/disk_image'
                os.makedirs(mount_dir, exist_ok=True)

                mount_cmd = [
                    'sudo', 'mount', '-o',
                    f'ro,loop,offset={byte_offset}',
                    os.path.join(ewf_mount_dir, 'ewf1'),
                    mount_dir
                ]

                mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)

            else:
                # Use mount for .dd images and other raw formats
                mount_dir = '/mnt/disk_image'
                os.makedirs(mount_dir, exist_ok=True)

                mount_cmd = [
                    'sudo', 'mount', '-o', 'loop,ro',
                    self.image_path, mount_dir
                ]

                mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)

            self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully.")
        except subprocess.CalledProcessError as e:
            self.operationCompleted.emit(False, f"Failed to mount the image on Linux. Error: {e.stderr}")
        except Exception as e:
            self.operationCompleted.emit(False, f"An unexpected error occurred: {str(e)}")

    def _dismount_image_linux(self):
        """Dismount image on Linux."""
        try:
            # Attempt to unmount the disk image
            disk_cmd = ['sudo', 'umount', '/mnt/disk_image']
            ewf_cmd = ['sudo', 'umount', '/mnt/ewf']

            try:
                # Try to unmount disk image
                subprocess.run(disk_cmd, check=True, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                logger.warning(f"Could not unmount disk image: {e.stderr}")

            try:
                # Try to unmount EWF
                subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)
            except subprocess.CalledProcessError as e:
                logger.warning(f"Could not unmount EWF: {e.stderr}")

            self.operationCompleted.emit(True, "Image was dismounted successfully.")
        except Exception as e:
            self.operationCompleted.emit(False, f"Failed to dismount the image on Linux. Error: {str(e)}")

    def _dismount_image_macos(self):
        """Dismount image on macOS using hdiutil."""
        try:
            # Get the list of currently mounted disk images
            info_cmd = ['hdiutil', 'info']
            info_process = subprocess.Popen(
                info_cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT
            )

            try:
                info_output, _ = info_process.communicate(timeout=INFO_TIMEOUT)
                if info_process.returncode != 0:
                    self.operationCompleted.emit(False, f"Failed to get mounted disks: {info_output.decode()}")
                    return
            except subprocess.TimeoutExpired:
                info_process.kill()
                self.operationCompleted.emit(False, "Getting disk info timed out")
                return

            info_output = info_output.decode()

            lines = info_output.splitlines()
            mounted_disks = []
            current_image_path = None

            # Parse the output to find the disk identifier for the given image path
            for line in lines:
                if 'image-path' in line:
                    current_image_path = line.split(': ')[1].strip()
                elif line.startswith('/dev/disk') and current_image_path == self.image_path:
                    disk_identifier = line.split()[0]
                    mounted_disks.append(disk_identifier)
                    current_image_path = None  # Reset after finding the corresponding disk

            if not mounted_disks:
                # If we're not targeting a specific image, try to unmount all mounted disks
                if not self.image_path:
                    for line in lines:
                        if line.startswith('/dev/disk'):
                            disk_identifier = line.split()[0]
                            mounted_disks.append(disk_identifier)

                if not mounted_disks:
                    self.operationCompleted.emit(False, "No mounted images found.")
                    return

            # Attempt to dismount all found disk identifiers
            success = False
            errors = []

            for disk_identifier in mounted_disks:
                try:
                    detach_cmd = ['hdiutil', 'detach', disk_identifier]
                    detach_process = subprocess.run(detach_cmd, check=True, capture_output=True, text=True)
                    success = True
                except subprocess.CalledProcessError:
                    try:
                        # If normal detach fails, attempt a forced detach
                        force_detach_cmd = ['hdiutil', 'detach', '-force', disk_identifier]
                        force_process = subprocess.run(force_detach_cmd, check=True, capture_output=True, text=True)
                        success = True
                    except subprocess.CalledProcessError as e:
                        errors.append(f"Failed to detach {disk_identifier}: {e.stderr}")

            if success:
                self.operationCompleted.emit(True, "Image was dismounted successfully.")
            else:
                self.operationCompleted.emit(False, "Failed to dismount all images: " + "; ".join(errors))

        except Exception as e:
            self.operationCompleted.emit(False, f"Failed to dismount the image on macOS: {str(e)}")

    def _dismount_image_windows(self):
        """Dismount image on Windows using Arsenal Image Mounter."""
        try:
            aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'
            if not os.path.exists(aim_path):
                self.operationCompleted.emit(False, "Arsenal Image Mounter not found. Please install it.")
                return

            cmd = [aim_path, '--dismount']

            # Use subprocess.run with proper error handling
            process = subprocess.run(
                cmd,
                check=True,
                capture_output=True,
                text=True,
                creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
            )

            self.operationCompleted.emit(True, "Image was dismounted successfully.")
        except subprocess.CalledProcessError as e:
            self.operationCompleted.emit(False, f"Failed to dismount the image on Windows. Error: {e.stderr}")
        except Exception as e:
            self.operationCompleted.emit(False, f"Unexpected error dismounting image: {str(e)}")

    def dismount_image(self):
        """Attempt to dismount the currently mounted image."""
        if self.is_running:
            self.showMessage.emit("Operation in Progress", "Please wait for the current operation to complete.")
            return

        self.operation = 'dismount'
        self.start()

    def mount_image(self):
        """Attempt to mount an image after prompting the user to select one."""
        if self.is_running:
            self.showMessage.emit("Operation in Progress", "Please wait for the current operation to complete.")
            return

        system = platform.system()

        if system == 'Darwin':  # macOS
            # Only allow .raw and .dd files on macOS
            supported_formats = "Raw Files (*.raw *.dd);;All Files (*)"
            valid_extensions = ['.raw', '.dd']
        else:
            # Original behavior for other operating systems
            supported_formats = (
                "EWF Files (*.E01);;Raw Files (*.dd);;AFF4 Files (*.aff4);;"
                "VHD Files (*.vhd);;VDI Files (*.vdi);;XVA Files (*.xva);;"
                "VMDK Files (*.vmdk);;OVA Files (*.ova);;QCOW Files (*.qcow *.qcow2);;All Files (*)"
            )
            valid_extensions = ['.e01', '.dd', '.aff4', '.vhd', '.vdi', '.xva', '.vmdk', '.ova', '.qcow', '.qcow2']

        while True:
            image_path, _ = QFileDialog.getOpenFileName(QWidget(None), "Select Disk Image", "", supported_formats)

            if not image_path:
                return  # No image was selected, so just exit the function

            file_extension = os.path.splitext(image_path)[1].lower()
            if file_extension in valid_extensions:
                break  # Exit the loop if a valid image was selected
            else:
                # Show an error message for an invalid file
                QMessageBox.warning(QWidget(None), "Invalid File Type", "The selected file is not a valid disk image.")

        # Normalize the path
        self.image_path = os.path.normpath(image_path)
        self.file_name = os.path.basename(self.image_path)
        self.operation = 'mount'
        self.start()


# ==================== FILE SEARCH WIDGET CLASSES ====================
class SizeTableWidgetItem(QTableWidgetItem):
    """Custom table widget item for proper size sorting."""
    def __lt__(self, other):
        return int(self.data(Qt.UserRole)) < int(other.data(Qt.UserRole))




class MainWindow(QMainWindow):
    # Class variable for icon caching
    _icon_cache = {}

    def __init__(self):
        super().__init__()

        # Create a database manager for icon lookup
        self.db_manager = DatabaseManager('tools/new_database_mappings.db')

        # Initialize variables for tracking
        self.current_selected_data = None
        self.current_offset = None
        self.current_path = "/"  # Initialize current path
        self.image_handler = None
        self._directory_cache = {}

        # Search/Browse mode state management
        self._search_mode = False  # False = Browse mode, True = Search mode
        self._search_query = ""  # Current search query
        self._last_browsed_state = {}  # Store last directory state for restoration

        # Search debounce timer - wait for user to stop typing before searching
        self._search_timer = QTimer()
        self._search_timer.setSingleShot(True)
        self._search_timer.setInterval(500)  # 500ms delay after last keystroke
        self._search_timer.timeout.connect(self._execute_search)

        # Directory navigation history (for Back/Forward buttons like Windows 11)
        self._directory_history = []  # List of visited directories: [(offset, inode, path), ...]
        self._history_index = -1  # Current position in history (-1 = no history)
        self._navigating_history = False  # Flag to prevent adding to history during Back/Forward

        # Load configuration
        self.api_keys = configparser.ConfigParser()
        try:
            self.api_keys.read('config.ini')
        except Exception as e:
            logger.error(f"Error loading configuration: {e}")

        # Initialize instance attributes
        self.image_mounted = False
        self.current_offset = None
        self.current_image_path = None
        self.image_manager = ImageManager()
        self.current_selected_data = None

        self.evidence_files = []

        # Connect to named method instead of complex lambda
        self.image_manager.operationCompleted.connect(self._handle_mount_operation_complete)

        self.initialize_ui()

    # ==================== HELPER METHODS ====================

    def _handle_mount_operation_complete(self, success: bool, message: str) -> None:
        """Handle completion of mount/dismount operation."""
        if success:
            QMessageBox.information(self, "Image Operation", message)
            self.image_mounted = not self.image_mounted
        else:
            QMessageBox.critical(self, "Image Operation", message)

    def _get_file_icon(self, file_extension: str) -> QIcon:
        """Get icon for file extension with caching."""
        if file_extension not in self._icon_cache:
            icon_path = self.db_manager.get_icon_path('file', file_extension)
            self._icon_cache[file_extension] = QIcon(icon_path)
        return self._icon_cache[file_extension]

    def _format_partition_text(self, addr: int, desc: bytes, start: int, end: int, length: int, fs_type: str) -> str:
        """Format partition display text."""
        size_in_bytes = length * SECTOR_SIZE
        readable_size = self.image_handler.get_readable_size(size_in_bytes)
        desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc
        return f"vol{addr} ({desc_str}: {start}-{end}, Size: {readable_size}, FS: {fs_type})"

    def _confirm_exit(self) -> bool:
        """Ask user to confirm exit."""
        reply = QMessageBox.question(
            self, 'Exit Confirmation',
            'Are you sure you want to exit?',
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.No
        )
        return reply == QMessageBox.StandardButton.Yes

    def _handle_dismount_if_needed(self) -> None:
        """Dismount image if mounted and user confirms."""
        if not self.image_mounted:
            return

        reply = QMessageBox.question(
            self, 'Dismount Image',
            'Do you want to dismount the mounted image before exiting?',
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.Yes
        )

        if reply == QMessageBox.StandardButton.Yes:
            self.image_manager.dismount_image()


    def _create_tree_item_for_entry(self, parent_item: QTreeWidgetItem, entry: Dict[str, Any],
                                    start_offset: int) -> QTreeWidgetItem:
        """Create tree item for a directory entry."""
        child_item = QTreeWidgetItem(parent_item)
        child_item.setText(0, entry["name"])

        if entry["is_directory"]:
            self._setup_directory_tree_item(child_item, entry, start_offset)
        else:
            self._setup_file_tree_item(child_item, entry, start_offset)

        return child_item

    def _setup_directory_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],
                                   start_offset: int) -> None:
        """Configure tree item for a directory entry."""
        # Check if directory has children
        sub_entries = self.image_handler.get_directory_contents(start_offset, entry["inode_number"])
        has_sub_entries = bool(sub_entries)

        # Set directory icon and data
        icon_path = self.db_manager.get_icon_path('folder', 'folder')
        item.setIcon(0, QIcon(icon_path))
        item.setData(0, Qt.UserRole, {
            "inode_number": entry["inode_number"],
            "type": 'directory',
            "start_offset": start_offset,
            "name": entry["name"]
        })

        # Set child indicator
        item.setChildIndicatorPolicy(
            QTreeWidgetItem.ShowIndicator if has_sub_entries
            else QTreeWidgetItem.DontShowIndicatorWhenChildless
        )

    def _setup_file_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],
                             start_offset: int) -> None:
        """Configure tree item for a file entry."""
        # Get file extension for icon
        file_extension = entry["name"].split('.')[-1].lower() if '.' in entry["name"] else 'unknown'

        # Use cached icon lookup
        icon = self._get_file_icon(file_extension)
        item.setIcon(0, icon)
        item.setData(0, Qt.UserRole, {
            "inode_number": entry["inode_number"],
            "type": 'file',
            "start_offset": start_offset,
            "name": entry["name"]
        })

    def _populate_table_entry(self, row_position: int, entry: Dict[str, Any], offset: int) -> None:
        """Populate a single table row with entry data."""
        entry_name = entry.get("name", "")
        inode_number = entry.get("inode_number", 0)
        is_directory = entry.get("is_directory", False)
        description = "Dir" if is_directory else "File"
        size_in_bytes = entry.get("size", 0)
        readable_size = self.image_handler.get_readable_size(size_in_bytes)
        created = entry.get("created", "N/A")
        accessed = entry.get("accessed", "N/A")
        modified = entry.get("modified", "N/A")
        changed = entry.get("changed", "N/A")

        icon_type = 'folder' if is_directory else 'file'
        icon_name = 'folder' if is_directory else (
            entry_name.split('.')[-1].lower() if '.' in entry_name else 'unknown')

        parent_inode = self.current_selected_data.get("inode_number") if self.current_selected_data else None

        self.listing_table.insertRow(row_position)
        self.insert_row_into_listing_table(entry_name, inode_number, description,
                                          icon_name, icon_type, offset,
                                          readable_size, created, accessed,
                                          modified, changed, parent_inode)

    # ==================== END HELPER METHODS ====================

    def initialize_ui(self):
        self.setWindowTitle('Trace 1.2.0')

        # Set application icon for all platforms
        app_icon = QIcon('Icons/logo_prev_ui.png')
        self.setWindowIcon(app_icon)

        # Set taskbar/dock icon for different platforms
        if os.name == 'nt':  # Windows
            import ctypes
            myappid = 'Trace'
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
        else:  # macOS and Linux
            # For macOS and Linux, setting the app icon at application level
            QApplication.instance().setWindowIcon(app_icon)

        self.setGeometry(DEFAULT_WINDOW_X, DEFAULT_WINDOW_Y, DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)

        menu_bar = QMenuBar(self)
        file_actions = {
            'Add Evidence File': self.load_image_evidence,
            'Remove Evidence File': self.remove_image_evidence,
            'Image Mounting': self.image_manager.mount_image,
            'Image Unmounting': self.image_manager.dismount_image,
            'separator': None,  # This will add a separator
            'Exit': self.close
        }

        self.create_menu(menu_bar, 'File', file_actions)

        view_menu = QMenu('View', self)

        # Create the "Full Screen" action and connect it to the showFullScreen slot
        full_screen_action = QAction("Full Screen", self)
        full_screen_action.triggered.connect(self.showFullScreen)
        view_menu.addAction(full_screen_action)

        # Create the "Normal Screen" action and connect it to the showNormal slot
        normal_screen_action = QAction("Normal Screen", self)
        normal_screen_action.triggered.connect(self.showNormal)
        view_menu.addAction(normal_screen_action)

        # Add a separator
        view_menu.addSeparator()

        # **Add Theme Selection Actions**
        # Create an action group for themes
        theme_group = QActionGroup(self)
        theme_group.setExclusive(True)  # Only one theme can be selected at a time

        # Light Theme Action
        light_theme_action = QAction("Light Mode", self)
        light_theme_action.setCheckable(True)
        light_theme_action.setChecked(True)  # Set Light Theme as default
        light_theme_action.triggered.connect(lambda: self.apply_stylesheet('light'))
        theme_group.addAction(light_theme_action)
        view_menu.addAction(light_theme_action)

        # Dark Theme Action
        dark_theme_action = QAction("Dark Mode", self)
        dark_theme_action.setCheckable(True)
        dark_theme_action.triggered.connect(lambda: self.apply_stylesheet('dark'))
        theme_group.addAction(dark_theme_action)
        view_menu.addAction(dark_theme_action)

        # Add the view menu to the menu bar
        menu_bar.addMenu(view_menu)

        # **Apply the default stylesheet**
        self.apply_stylesheet('light')

        tools_menu = QMenu('Tools', self)

        verify_image_action = QAction("Verify Image", self)
        verify_image_action.triggered.connect(self.verify_image)
        tools_menu.addAction(verify_image_action)

        conversion_action = QAction("Convert E01 to DD/RAW", self)
        conversion_action.triggered.connect(self.show_conversion_widget)
        tools_menu.addAction(conversion_action)

        veriphone_api_action = QAction("Veriphone API", self)
        veriphone_api_action.triggered.connect(self.show_veriphone_widget)
        tools_menu.addAction(veriphone_api_action)

        # Add "Options" menu for API key configuration
        options_menu = QMenu('Options', self)
        api_key_action = QAction("API Keys", self)
        api_key_action.triggered.connect(self.show_api_key_dialog)
        options_menu.addAction(api_key_action)

        help_menu = QMenu('Help', self)
        help_menu.addAction("About")
        help_menu.triggered.connect(lambda: AboutDialog(self).exec_())

        menu_bar.addMenu(view_menu)
        menu_bar.addMenu(tools_menu)
        menu_bar.addMenu(options_menu)
        menu_bar.addMenu(help_menu)

        self.setMenuBar(menu_bar)

        self.main_toolbar = QToolBar()
        self.main_toolbar.setMovable(False)
        self.main_toolbar.setFloatable(False)
        self.main_toolbar.addAction(
            self.create_action('Icons/icons8-evidence-48.png', "Load Image", self.load_image_evidence))
        self.main_toolbar.addAction(
            self.create_action('Icons/icons8-evidence-96.png', "Remove Image", self.remove_image_evidence))
        self.main_toolbar.addSeparator()

        # Create verify_image_button as an attribute of MainWindow
        self.verify_image_button = self.create_action('Icons/icons8-verify-blue.png', "Verify Image", self.verify_image)
        self.main_toolbar.addAction(self.verify_image_button)

        self.main_toolbar.addSeparator()
        self.main_toolbar.addAction(
            self.create_action('Icons/devices/icons8-hard-disk-48.png', "Mount Image", self.image_manager.mount_image))
        self.main_toolbar.addAction(self.create_action('Icons/devices/icons8-hard-disk-48_red.png', "Unmount Image",
                                                       self.image_manager.dismount_image))

        # Navigation buttons (Back, Forward, Up) will be added to the listing search toolbar
        # Created later in the UI setup

        self.addToolBar(Qt.TopToolBarArea, self.main_toolbar)

        self.tree_viewer = QTreeWidget(self)
        self.tree_viewer.setIconSize(QSize(16, 16))
        self.tree_viewer.setHeaderHidden(True)
        self.tree_viewer.itemExpanded.connect(self.on_item_expanded)
        self.tree_viewer.itemClicked.connect(self.on_item_clicked)
        self.tree_viewer.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tree_viewer.customContextMenuRequested.connect(self.open_tree_context_menu)

        tree_dock = QDockWidget('Tree View', self)

        tree_dock.setWidget(self.tree_viewer)
        self.addDockWidget(Qt.LeftDockWidgetArea, tree_dock)

        self.result_viewer = QTabWidget(self)
        self.setCentralWidget(self.result_viewer)

        self.listing_table = QTableWidget()
        self.listing_table.setSortingEnabled(True)
        self.listing_table.verticalHeader().setVisible(False)
        self.listing_table.setObjectName("listingTable")  # Set object name for specific CSS styling

        # Set size policy to expand with window
        self.listing_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        # Use alternate row colors
        self.listing_table.setAlternatingRowColors(True)
        self.listing_table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.listing_table.setIconSize(QSize(24, 24))
        self.listing_table.setColumnCount(10)  # 10 columns: Name, Inode, Type, Size, 4 timestamps, Path, Info

        # Enable horizontal scrolling for smaller windows
        self.listing_table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
        self.listing_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        # Connect click event to handle navigation in search mode
        self.listing_table.itemClicked.connect(self.on_listing_table_item_clicked)

        # Create a QVBoxLayout for the listing tab
        self.listing_layout = QVBoxLayout()
        self.listing_layout.setContentsMargins(0, 0, 0, 0)  # Set to zero to remove margins
        self.listing_layout.setSpacing(0)  # Remove spacing between widgets

        # ==================== CREATE UNIFIED TOOLBAR (like File Carving tab) ====================
        self.listing_toolbar = QToolBar()
        self.listing_toolbar.setContentsMargins(0, 0, 0, 0)
        self.listing_toolbar.setMovable(False)

        # LEFT SIDE: Icon and Title
        self.listing_icon_label = QLabel()
        self.listing_icon_label.setPixmap(QPixmap('Icons/icons8-search-in-browser-50.png'))
        self.listing_icon_label.setFixedSize(48, 48)
        self.listing_toolbar.addWidget(self.listing_icon_label)

        self.listing_title_label = QLabel("File System Browser")
        self.listing_title_label.setStyleSheet("""
            QLabel {
                font-size: 20px;
                color: #37c6d0;
                font-weight: bold;
                margin-left: 8px;
            }
        """)
        self.listing_toolbar.addWidget(self.listing_title_label)

        # Add spacer after title
        title_spacer = QLabel()
        title_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.listing_toolbar.addWidget(title_spacer)

        # MIDDLE: Navigation buttons (Back, Forward, Up) - next to title
        self.back_action = QAction(QIcon("Icons/icons8-left-arrow-50.png"), "Back", self)
        self.back_action.triggered.connect(self.navigate_back)
        self.back_action.setEnabled(False)
        self.listing_toolbar.addAction(self.back_action)

        self.forward_action = QAction(QIcon("Icons/icons8-right-arrow-50.png"), "Forward", self)
        self.forward_action.triggered.connect(self.navigate_forward)
        self.forward_action.setEnabled(False)
        self.listing_toolbar.addAction(self.forward_action)

        self.go_up_action = QAction(QIcon("Icons/icons8-thick-arrow-pointing-up-50.png"), "Go Up Directory", self)
        self.go_up_action.triggered.connect(self.navigate_up_directory)
        self.go_up_action.setEnabled(False)
        self.listing_toolbar.addAction(self.go_up_action)

        # Add vertical separator after navigation buttons
        self.listing_toolbar.addSeparator()

        # RIGHT SIDE: Search functionality
        # Add search bar
        self.listing_search_bar = QLineEdit()
        self.listing_search_bar.setObjectName("listingSearchBar")
        self.listing_search_bar.setPlaceholderText("Search files (press Enter, supports wildcards: *.pdf, name.*)")
        self.listing_search_bar.setFixedHeight(35)
        self.listing_search_bar.setFixedWidth(450)
        # Only search when user presses Enter
        self.listing_search_bar.returnPressed.connect(self.trigger_listing_search)
        # Monitor text changes for auto-clearing results
        self.listing_search_bar.textChanged.connect(self.on_listing_search_text_changed)
        self.listing_toolbar.addWidget(self.listing_search_bar)

        # Add small end spacer
        end_spacer = QWidget()
        end_spacer.setFixedWidth(10)
        self.listing_toolbar.addWidget(end_spacer)

        # Add the single toolbar and listing table to the layout
        self.listing_layout.addWidget(self.listing_toolbar)  # Single unified toolbar
        self.listing_layout.addWidget(self.listing_table)  # Table below toolbar

        # Create a widget to hold the layout
        self.listing_widget = QWidget()
        self.listing_widget.setLayout(self.listing_layout)

        # Set the horizontal header with hybrid resizing approach
        header = self.listing_table.horizontalHeader()

        # All columns use Interactive mode (fixed width, manually resizable)
        # This enables horizontal scrolling on smaller windows
        header.setSectionResizeMode(0, QHeaderView.Interactive)  # Name - fixed, manually resizable
        header.setSectionResizeMode(1, QHeaderView.Interactive)  # Inode - fixed, manually resizable
        header.setSectionResizeMode(2, QHeaderView.Interactive)  # Type - fixed, manually resizable
        header.setSectionResizeMode(3, QHeaderView.Interactive)  # Size - fixed, manually resizable
        header.setSectionResizeMode(4, QHeaderView.Interactive)  # Created - fixed, manually resizable
        header.setSectionResizeMode(5, QHeaderView.Interactive)  # Accessed - fixed, manually resizable
        header.setSectionResizeMode(6, QHeaderView.Interactive)  # Modified - fixed, manually resizable
        header.setSectionResizeMode(7, QHeaderView.Interactive)  # Changed - fixed, manually resizable
        header.setSectionResizeMode(8, QHeaderView.Interactive)  # Path - fixed, manually resizable
        header.setSectionResizeMode(9, QHeaderView.Interactive)  # Info - fixed, manually resizable

        # Set initial column widths
        self.listing_table.setColumnWidth(0, COLUMN_WIDTHS['name'])      # Name - 400px (widest)
        self.listing_table.setColumnWidth(1, COLUMN_WIDTHS['inode'])     # Inode - 45px
        self.listing_table.setColumnWidth(2, COLUMN_WIDTHS['type'])      # Type - 50px
        self.listing_table.setColumnWidth(3, COLUMN_WIDTHS['size'])      # Size - 70px
        self.listing_table.setColumnWidth(4, COLUMN_WIDTHS['created'])   # Created - 90px (narrower)
        self.listing_table.setColumnWidth(5, COLUMN_WIDTHS['accessed'])  # Accessed - 90px (narrower)
        self.listing_table.setColumnWidth(6, COLUMN_WIDTHS['modified'])  # Modified - 90px (narrower)
        self.listing_table.setColumnWidth(7, COLUMN_WIDTHS['changed'])   # Changed - 90px (narrower)
        self.listing_table.setColumnWidth(8, COLUMN_WIDTHS['path'])      # Path - 300px (wide)
        self.listing_table.setColumnWidth(9, 250)                        # Info - 250px (for volumes)

        # Remove any extra space in the header
        header.setStyleSheet("QHeaderView::section { margin-top: 0px; padding-top: 2px; }")
        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        # Set the header labels
        self.listing_table.setHorizontalHeaderLabels(
            ['Name', 'Inode', 'Type', 'Size', 'Created Date', 'Accessed Date', 'Modified Date', 'Changed Date', 'Path', 'Info']
        )

        self.listing_table.itemDoubleClicked.connect(self.on_listing_table_item_clicked)
        self.listing_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.listing_table.customContextMenuRequested.connect(self.open_listing_context_menu)
        self.listing_table.setSelectionBehavior(QTableWidget.SelectRows)

        # Set the color of the selected row
        palette = self.listing_table.palette()
        palette.setBrush(QPalette.Highlight, QBrush(Qt.lightGray))  # Change Qt.lightGray to your preferred color
        self.listing_table.setPalette(palette)

        header = self.listing_table.horizontalHeader()
        header.setDefaultAlignment(Qt.AlignLeft)

        self.result_viewer.addTab(self.listing_widget, 'Listing')

        self.deleted_files_widget = FileCarvingWidget(self)
        self.result_viewer.addTab(self.deleted_files_widget, 'Deleted Files')

        self.registry_extractor_widget = RegistryExtractor(self.image_handler)
        self.result_viewer.addTab(self.registry_extractor_widget, 'Registry')

        self.viewer_tab = QTabWidget(self)

        self.hex_viewer = HexViewer(self)
        self.viewer_tab.addTab(self.hex_viewer, 'Hex')

        self.text_viewer = TextViewer(self)
        self.viewer_tab.addTab(self.text_viewer, 'Text')

        self.application_viewer = UnifiedViewer(self)
        self.application_viewer.layout.setContentsMargins(0, 0, 0, 0)
        self.application_viewer.layout.setSpacing(0)
        self.viewer_tab.addTab(self.application_viewer, 'Application')

        self.metadata_viewer = MetadataViewer(self.image_handler)
        self.viewer_tab.addTab(self.metadata_viewer, 'File Metadata')

        self.exif_viewer = ExifViewer(self)
        self.viewer_tab.addTab(self.exif_viewer, 'Exif Data')

        self.virus_total_api = VirusTotal()
        self.viewer_tab.addTab(self.virus_total_api, 'Virus Total API')

        # Set the API key if it exists
        virus_total_key = self.api_keys.get('API_KEYS', 'virustotal', fallback='')
        self.virus_total_api.set_api_key(virus_total_key)

        self.viewer_dock = QDockWidget('Utils', self)
        self.viewer_dock.setWidget(self.viewer_tab)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.viewer_dock)

        self.viewer_dock.setMinimumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)
        self.viewer_dock.setMaximumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)
        self.viewer_dock.visibilityChanged.connect(self.on_viewer_dock_focus)
        self.viewer_tab.currentChanged.connect(self.display_content_for_active_tab)

        # disable all tabs before loading an image file
        self.enable_tabs(False)

    def apply_stylesheet(self, theme='light'):
        if theme == 'dark':
            qss_file = 'styles/dark_theme.qss'
        else:
            qss_file = 'styles/light_theme.qss'  # Ensure your existing QSS file is named 'light_theme.qss'

        try:
            with open(qss_file, 'r') as f:
                stylesheet = f.read()
            QApplication.instance().setStyleSheet(stylesheet)
        except Exception as e:
            logger.error(f"Error loading stylesheet {qss_file}: {e}")

    def show_api_key_dial
Download .txt
gitextract_yykb5mfu/

├── .gitignore
├── LICENSE
├── README.md
├── install_macos_linux_WSL.sh
├── main.py
├── modules/
│   ├── about.py
│   ├── converter.py
│   ├── exif_tab.py
│   ├── file_carving.py
│   ├── hex_tab.py
│   ├── mainwindow.py
│   ├── metadata_tab.py
│   ├── registry.py
│   ├── text_tab.py
│   ├── unified_application_manager.py
│   ├── verification.py
│   ├── veriphone_api.py
│   └── virus_total_tab.py
├── requirements.txt
├── requirements_macos_silicon.txt
├── styles/
│   ├── dark_theme.qss
│   └── light_theme.qss
└── tools/
    ├── Arsenal-Image-Mounter-v3.10.257/
    │   ├── Arsenal Recon - End User License Agreement.txt
    │   ├── ArsenalImageMounter.deps.json
    │   ├── ArsenalImageMounter.log
    │   ├── ArsenalImageMounter.runtimeconfig.json
    │   ├── aim_cli.runtimeconfig.json
    │   ├── readme.txt
    │   └── readme_cli.txt
    └── sleuthkit-4.12.1-win32/
        ├── NEWS.txt
        ├── README-win32.txt
        ├── README.txt
        ├── bin/
        │   └── mactime.pl
        ├── lib/
        │   ├── libtsk.lib
        │   └── libtsk_jni.lib
        └── licenses/
            ├── IBM-LICENSE
            └── cpl1.0.txt
Download .txt
SYMBOL INDEX (469 symbols across 13 files)

FILE: modules/about.py
  class AboutDialog (line 6) | class AboutDialog(QDialog):
    method __init__ (line 7) | def __init__(self, parent=None):

FILE: modules/converter.py
  function list_drives (line 18) | def list_drives():
  class Main (line 34) | class Main(QMainWindow):
    method __init__ (line 35) | def __init__(self):
    method init_ui (line 47) | def init_ui(self):
    method show_specific_widget (line 60) | def show_specific_widget(self, widget_name):
    method show_select_source (line 73) | def show_select_source(self):
  class DriveSelectionWidget (line 78) | class DriveSelectionWidget(QWidget):
    method __init__ (line 82) | def __init__(self, parent=None):
    method init_ui (line 86) | def init_ui(self):
    method on_select_clicked (line 107) | def on_select_clicked(self):
  class SelectSourceDialog (line 112) | class SelectSourceDialog(QWidget):
    method __init__ (line 115) | def __init__(self, parent=None):
    method on_next_clicked (line 145) | def on_next_clicked(self):
  class ConversionWidget (line 156) | class ConversionWidget(QWidget):
    method __init__ (line 159) | def __init__(self, parent=None):
    method init_ui (line 166) | def init_ui(self):
    method on_back_clicked (line 202) | def on_back_clicked(self):
    method browse_file (line 206) | def browse_file(self):
    method select_output_dir (line 211) | def select_output_dir(self):
    method convert (line 216) | def convert(self):
    method perform_conversion (line 238) | def perform_conversion(self, input_path, output_path):

FILE: modules/exif_tab.py
  class ExifViewerManager (line 8) | class ExifViewerManager:
    method __init__ (line 9) | def __init__(self):
    method get_exif_data_from_content (line 13) | def get_exif_data_from_content(file_content):
    method load_exif_data (line 29) | def load_exif_data(self, file_content):
  class ExifViewer (line 49) | class ExifViewer(QWidget):
    method __init__ (line 50) | def __init__(self, parent=None):
    method init_ui (line 56) | def init_ui(self):
    method display_exif_data (line 72) | def display_exif_data(self, exif_data):
    method clear_content (line 115) | def clear_content(self):
    method load_and_display_exif_data (line 119) | def load_and_display_exif_data(self, file_content):

FILE: modules/file_carving.py
  class NumericTableWidgetItem (line 27) | class NumericTableWidgetItem(QTableWidgetItem):
    method __lt__ (line 28) | def __lt__(self, other):
  class FileCarvingWidget (line 42) | class FileCarvingWidget(QWidget):
    method __init__ (line 45) | def __init__(self, parent=None):
    method init_ui (line 55) | def init_ui(self):
    method create_table_widget (line 117) | def create_table_widget(self):
    method create_list_widget (line 169) | def create_list_widget(self):
    method center_crop_to_square (line 219) | def center_crop_to_square(pixmap, target_size):
    method render_svg_to_pixmap (line 245) | def render_svg_to_pixmap(svg_path, target_size):
    method set_icon_size (line 263) | def set_icon_size(self, size):
    method set_small_size (line 269) | def set_small_size(self):
    method set_medium_size (line 272) | def set_medium_size(self):
    method set_large_size (line 275) | def set_large_size(self):
    method start_carving (line 278) | def start_carving(self):
    method stop_carving (line 329) | def stop_carving(self):
    method set_image_handler (line 334) | def set_image_handler(self, image_handler):
    method is_offset_allocated (line 339) | def is_offset_allocated(offset, chunk_size, allocation_map):
    method open_context_menu (line 368) | def open_context_menu(self, position):
    method open_image (line 381) | def open_image(self):
    method open_file_location (line 395) | def open_file_location(self):
    method get_carved_timestamp (line 405) | def get_carved_timestamp(self, file_name):
    method on_carved_file_clicked (line 412) | def on_carved_file_clicked(self, *args):
    method setup_buttons (line 479) | def setup_buttons(self):
    method start_carving_thread (line 485) | def start_carving_thread(self):
    method stop_carving_thread (line 491) | def stop_carving_thread(self):
    method is_valid_file (line 496) | def is_valid_file(self, data, file_type):
    method carve_pdf_files (line 521) | def carve_pdf_files(self, chunk, global_offset):
    method carve_wav_files (line 556) | def carve_wav_files(self, chunk, offset):
    method carve_mov_files (line 581) | def carve_mov_files(self, chunk, offset):
    method carve_jpg_files (line 631) | def carve_jpg_files(self, chunk, offset):
    method carve_gif_files (line 652) | def carve_gif_files(self, chunk, offset):
    method carve_png_files (line 673) | def carve_png_files(self, chunk, offset):
    method carve_wmv_files (line 694) | def carve_wmv_files(self, chunk, offset):
    method carve_zip_files (line 729) | def carve_zip_files(self, chunk, global_offset):
    method carve_bmp_files (line 774) | def carve_bmp_files(self, chunk, offset):
    method carve_files (line 817) | def carve_files(self, selected_file_types):
    method extract_original_timestamp (line 885) | def extract_original_timestamp(file_content, file_type):
    method save_file (line 936) | def save_file(self, file_content, file_type, file_path, offset):
    method display_carved_file (line 968) | def display_carved_file(self, name, size, type_, modification_date, fi...
    method clear (line 1053) | def clear(self):
    method clear_ui (line 1060) | def clear_ui(self):
    method handle_resize_event (line 1064) | def handle_resize_event(self, event):

FILE: modules/hex_tab.py
  class SearchWorker (line 12) | class SearchWorker(QObject):
    method __init__ (line 15) | def __init__(self, hex_viewer_manager, query):
    method run (line 20) | def run(self):
  class HexViewerManager (line 25) | class HexViewerManager:
    method __init__ (line 28) | def __init__(self, hex_content, byte_content):
    method format_hex (line 36) | def format_hex(self, page=0):
    method format_hex_chunk (line 47) | def format_hex_chunk(self, start):
    method total_pages (line 63) | def total_pages(self):
    method search (line 66) | def search(self, query):
    method search_by_address (line 79) | def search_by_address(self, address):
    method search_by_string (line 91) | def search_by_string(self, query):
    method search_by_hex (line 107) | def search_by_hex(self, hex_query):
  class HexViewer (line 129) | class HexViewer(QWidget):
    method __init__ (line 130) | def __init__(self, parent=None):
    method show_context_menu (line 146) | def show_context_menu(self, pos):
    method copy_to_clipboard (line 150) | def copy_to_clipboard(self):
    method initialize_ui (line 176) | def initialize_ui(self):
    method resizeEvent (line 233) | def resizeEvent(self, event: QResizeEvent):
    method setup_toolbar (line 244) | def setup_toolbar(self):
    method update_font_size (line 351) | def update_font_size(self):
    method setup_hex_table (line 393) | def setup_hex_table(self):
    method display_hex_content (line 445) | def display_hex_content(self, file_content):
    method export_content (line 458) | def export_content(self, selected_format):
    method export_as_text (line 479) | def export_as_text(self, file_name):
    method export_as_html (line 492) | def export_as_html(self, file_name):
    method parse_hex_line (line 517) | def parse_hex_line(self, line):
    method clear_content (line 524) | def clear_content(self):
    method load_first_page (line 527) | def load_first_page(self):
    method load_last_page (line 534) | def load_last_page(self):
    method next_page (line 541) | def next_page(self):
    method previous_page (line 549) | def previous_page(self):
    method search_result_clicked (line 557) | def search_result_clicked(self, item):
    method display_current_page (line 561) | def display_current_page(self):
    method go_to_page_by_entry (line 612) | def go_to_page_by_entry(self):
    method update_navigation_states (line 624) | def update_navigation_states(self):
    method update_total_pages_label (line 635) | def update_total_pages_label(self):
    method trigger_search (line 640) | def trigger_search(self):
    method cleanup_thread_resources (line 664) | def cleanup_thread_resources(self):
    method closeEvent (line 673) | def closeEvent(self, event):
    method handle_search_results (line 679) | def handle_search_results(self, matches):
    method navigate_to_address (line 695) | def navigate_to_address(self, address):

FILE: modules/mainwindow.py
  function safe_datetime (line 104) | def safe_datetime(timestamp):
  class FileSystemUtils (line 114) | class FileSystemUtils:
    method get_readable_size (line 116) | def get_readable_size(size_in_bytes):
    method temp_file (line 129) | def temp_file():
  class EWFImgInfo (line 142) | class EWFImgInfo(pytsk3.Img_Info):
    method __init__ (line 143) | def __init__(self, ewf_handle):
    method close (line 147) | def close(self):
    method read (line 150) | def read(self, offset, size):
    method get_size (line 154) | def get_size(self):
  class ImageHandler (line 159) | class ImageHandler:
    method __init__ (line 160) | def __init__(self, image_path):
    method __del__ (line 173) | def __del__(self):
    method close_resources (line 177) | def close_resources(self):
    method get_size (line 200) | def get_size(self):
    method read (line 207) | def read(self, offset, size):
    method build_allocation_map (line 214) | def build_allocation_map(self, start_offset):
    method get_image_type (line 313) | def get_image_type(self):
    method calculate_hashes (line 330) | def calculate_hashes(self, progress_callback=None):
    method load_image (line 425) | def load_image(self):
    method has_filesystem (line 458) | def has_filesystem(self, start_offset):
    method is_wiped (line 462) | def is_wiped(self):
    method partitions (line 467) | def partitions(self):
    method get_partitions (line 473) | def get_partitions(self):
    method _get_partitions (line 477) | def _get_partitions(self):
    method get_fs_info (line 488) | def get_fs_info(self, start_offset):
    method get_fs_type (line 499) | def get_fs_type(self, start_offset):
    method check_partition_contents (line 523) | def check_partition_contents(self, partition_start_offset):
    method get_directory_contents (line 536) | def get_directory_contents(self, start_offset, inode_number=None):
    method get_registry_hive (line 583) | def get_registry_hive(self, fs_info, hive_path):
    method get_windows_version (line 593) | def get_windows_version(self, start_offset):
    method read_unallocated_space (line 638) | def read_unallocated_space(self, start_offset, end_offset):
    method open_image (line 673) | def open_image(self):
    method list_files (line 682) | def list_files(self, extensions=None):
    method process_partition (line 698) | def process_partition(self, img_info, offset_sectors, files_list, exte...
    method _recursive_file_search (line 706) | def _recursive_file_search(self, fs_info, directory, parent_path, file...
    method _get_directory_metadata (line 765) | def _get_directory_metadata(self, entry, parent_path, start_offset=0):
    method _get_volume_name_for_offset (line 807) | def _get_volume_name_for_offset(self, start_offset):
    method _get_file_metadata (line 820) | def _get_file_metadata(self, entry, parent_path, start_offset=0):
    method search_files (line 863) | def search_files(self, search_query=None):
    method process_partition_search (line 886) | def process_partition_search(self, img_info, offset_sectors, files_lis...
    method get_file_content (line 898) | def get_file_content(self, inode_number, offset):
    method get_readable_size (line 932) | def get_readable_size(self, size_in_bytes):
  class DatabaseManager (line 938) | class DatabaseManager:
    method __init__ (line 939) | def __init__(self, db_path):
    method _connect (line 945) | def _connect(self):
    method __del__ (line 955) | def __del__(self):
    method close (line 959) | def close(self):
    method get_icon_path (line 968) | def get_icon_path(self, icon_type, identifier):
  class ImageManager (line 1020) | class ImageManager(QThread):
    method __init__ (line 1025) | def __init__(self):
    method __del__ (line 1033) | def __del__(self):
    method cleanup_resources (line 1036) | def cleanup_resources(self):
    method run (line 1045) | def run(self):
    method _mount_image_windows (line 1073) | def _mount_image_windows(self):
    method _mount_image_macos (line 1113) | def _mount_image_macos(self):
    method _mount_image_linux (line 1198) | def _mount_image_linux(self):
    method _dismount_image_linux (line 1267) | def _dismount_image_linux(self):
    method _dismount_image_macos (line 1290) | def _dismount_image_macos(self):
    method _dismount_image_windows (line 1364) | def _dismount_image_windows(self):
    method dismount_image (line 1389) | def dismount_image(self):
    method mount_image (line 1398) | def mount_image(self):
  class SizeTableWidgetItem (line 1440) | class SizeTableWidgetItem(QTableWidgetItem):
    method __lt__ (line 1442) | def __lt__(self, other):
  class MainWindow (line 1448) | class MainWindow(QMainWindow):
    method __init__ (line 1452) | def __init__(self):
    method _handle_mount_operation_complete (line 1504) | def _handle_mount_operation_complete(self, success: bool, message: str...
    method _get_file_icon (line 1512) | def _get_file_icon(self, file_extension: str) -> QIcon:
    method _format_partition_text (line 1519) | def _format_partition_text(self, addr: int, desc: bytes, start: int, e...
    method _confirm_exit (line 1526) | def _confirm_exit(self) -> bool:
    method _handle_dismount_if_needed (line 1536) | def _handle_dismount_if_needed(self) -> None:
    method _create_tree_item_for_entry (line 1552) | def _create_tree_item_for_entry(self, parent_item: QTreeWidgetItem, en...
    method _setup_directory_tree_item (line 1565) | def _setup_directory_tree_item(self, item: QTreeWidgetItem, entry: Dic...
    method _setup_file_tree_item (line 1588) | def _setup_file_tree_item(self, item: QTreeWidgetItem, entry: Dict[str...
    method _populate_table_entry (line 1604) | def _populate_table_entry(self, row_position: int, entry: Dict[str, An...
    method initialize_ui (line 1631) | def initialize_ui(self):
    method apply_stylesheet (line 1967) | def apply_stylesheet(self, theme='light'):
    method show_api_key_dialog (line 1980) | def show_api_key_dialog(self):
    method save_api_keys (line 2016) | def save_api_keys(self, virus_total_key, veriphone_key, dialog):
    method show_conversion_widget (line 2036) | def show_conversion_widget(self):
    method show_veriphone_widget (line 2041) | def show_veriphone_widget(self):
    method verify_image (line 2050) | def verify_image(self):
    method on_verification_closed (line 2064) | def on_verification_closed(self, event):
    method enable_tabs (line 2076) | def enable_tabs(self, state):
    method create_menu (line 2083) | def create_menu(self, menu_bar, menu_name, actions):
    method create_tree_item (line 2095) | def create_tree_item(parent, text, icon_path, data):
    method on_viewer_dock_focus (line 2102) | def on_viewer_dock_focus(self, visible):
    method clear_ui (line 2110) | def clear_ui(self):
    method clear_viewers (line 2131) | def clear_viewers(self):
    method closeEvent (line 2139) | def closeEvent(self, event):
    method cleanup_resources (line 2151) | def cleanup_resources(self):
    method load_image_evidence (line 2214) | def load_image_evidence(self):
    method remove_image_evidence (line 2282) | def remove_image_evidence(self):
    method remove_from_tree_viewer (line 2314) | def remove_from_tree_viewer(self, evidence_name):
    method load_partitions_into_tree (line 2322) | def load_partitions_into_tree(self, image_path):
    method populate_contents (line 2376) | def populate_contents(self, item: QTreeWidgetItem, data: Dict[str, Any...
    method on_item_expanded (line 2386) | def on_item_expanded(self, item):
    class FileContentWorker (line 2400) | class FileContentWorker(QThread):
      method __init__ (line 2405) | def __init__(self, image_handler, inode_number, offset):
      method run (line 2411) | def run(self):
    class MediaStreamWorker (line 2422) | class MediaStreamWorker(QThread):
      method __init__ (line 2426) | def __init__(self, image_handler, inode_number, offset):
      method run (line 2432) | def run(self):
    class UnallocatedSpaceWorker (line 2460) | class UnallocatedSpaceWorker(QThread):
      method __init__ (line 2464) | def __init__(self, image_handler, start_offset, end_offset):
      method run (line 2470) | def run(self):
    method on_item_clicked (line 2480) | def on_item_clicked(self, item, column):
    method update_directory_up_button (line 2586) | def update_directory_up_button(self):
    method find_parent_inode (line 2612) | def find_parent_inode(self, start_offset, inode_number):
    method navigate_up_directory (line 2634) | def navigate_up_directory(self):
    method _add_to_history (line 2701) | def _add_to_history(self, directory_data):
    method _update_navigation_buttons (line 2732) | def _update_navigation_buttons(self):
    method navigate_back (line 2742) | def navigate_back(self):
    method navigate_forward (line 2763) | def navigate_forward(self):
    method _navigate_to_history_entry (line 2784) | def _navigate_to_history_entry(self, history_entry):
    method select_tree_item_by_inode (line 2823) | def select_tree_item_by_inode(self, inode_number, start_offset):
    method find_tree_item_recursive (line 2849) | def find_tree_item_recursive(self, parent_item, inode_number, start_of...
    method display_volumes_in_listing (line 2872) | def display_volumes_in_listing(self) -> None:
    method populate_listing_table (line 2995) | def populate_listing_table(self, entries: List[Dict[str, Any]], offset...
    method insert_row_into_listing_table (line 3047) | def insert_row_into_listing_table(self, entry_name, entry_inode, descr...
    method update_viewer_with_file_content (line 3091) | def update_viewer_with_file_content(self, file_content, data):
    method update_viewer_with_media_stream (line 3128) | def update_viewer_with_media_stream(self, file_obj, file_size, metadat...
    method display_content_for_active_tab (line 3159) | def display_content_for_active_tab(self):
    method open_listing_context_menu (line 3229) | def open_listing_context_menu(self, position):
    method handle_export (line 3258) | def handle_export(self, data, dest_dir):
    method log_error (line 3299) | def log_error(self, message):
    method open_tree_context_menu (line 3304) | def open_tree_context_menu(self, position):
    method view_os_information (line 3325) | def view_os_information(self, index):
    method _populate_volume_table (line 3551) | def _populate_volume_table(self, table, partitions):
    method _extract_comprehensive_volume_info (line 3630) | def _extract_comprehensive_volume_info(self, start_offset):
    method _get_image_info (line 3674) | def _get_image_info(self):
    method _get_partition_info (line 3728) | def _get_partition_info(self, partition):
    method _get_filesystem_colors (line 3798) | def _get_filesystem_colors(self):
    method _create_space_allocation_chart (line 3816) | def _create_space_allocation_chart(self):
    method create_action (line 3916) | def create_action(self, icon_path, text, callback):
    method get_grandparent_inode (line 3921) | def get_grandparent_inode(self, parent_inode, start_offset):
    method on_listing_table_item_clicked (line 3946) | def on_listing_table_item_clicked(self, item):
    method navigate_tree_to_path (line 3970) | def navigate_tree_to_path(self, path, file_data):
    method on_listing_search_text_changed (line 4052) | def on_listing_search_text_changed(self):
    method trigger_listing_search (line 4060) | def trigger_listing_search(self):
    method _execute_search (line 4079) | def _execute_search(self):
    method clear_listing_search (line 4085) | def clear_listing_search(self):
    method switch_to_search_mode (line 4092) | def switch_to_search_mode(self):
    method switch_to_browse_mode (line 4120) | def switch_to_browse_mode(self):
    method _restore_tree_selection (line 4157) | def _restore_tree_selection(self, path, directory_data):
    method _wildcard_to_regex (line 4225) | def _wildcard_to_regex(self, pattern):
    method _matches_wildcard (line 4234) | def _matches_wildcard(self, filename, pattern):
    method perform_search (line 4239) | def perform_search(self, search_query):
    method insert_search_result_row (line 4286) | def insert_search_result_row(self, file_data):
    method apply_browse_filter (line 4333) | def apply_browse_filter(self, extensions):
    method open_search_result_file (line 4368) | def open_search_result_file(self, file_data):
    method show_file_in_directory (line 4374) | def show_file_in_directory(self, file_data):
    method on_listing_table_item_clicked (line 4432) | def on_listing_table_item_clicked(self, item):
  class ExportWorker (line 4518) | class ExportWorker(QThread):
    method __init__ (line 4524) | def __init__(self, image_handler, inode_number, offset, dest_dir, name...
    method run (line 4535) | def run(self):
    method _export_directory (line 4546) | def _export_directory(self, inode_number, offset, dest_dir, name):
    method _count_items_recursive (line 4575) | def _count_items_recursive(self, entries, offset):
    method _export_item (line 4585) | def _export_item(self, inode_number, offset, dest_dir, name, is_direct...
    method _export_file (line 4612) | def _export_file(self, inode_number, offset, dest_dir, name):

FILE: modules/metadata_tab.py
  class MetadataViewer (line 9) | class MetadataViewer(QWidget):
    method __init__ (line 10) | def __init__(self, image_handler):
    method init_ui (line 15) | def init_ui(self):
    method display_metadata (line 27) | def display_metadata(self, data):
    method run_istat (line 127) | def run_istat(self, offset, inode_number, image_path):
    method clear (line 159) | def clear(self):

FILE: modules/registry.py
  class RegistryExtractor (line 13) | class RegistryExtractor(QWidget):
    method __init__ (line 14) | def __init__(self, image_handler):
    method init_ui (line 22) | def init_ui(self):
    method onCustomContextMenuRequested (line 92) | def onCustomContextMenuRequested(self, position):
    method load_selected_hive (line 107) | def load_selected_hive(self):
    method display_registry_hive (line 141) | def display_registry_hive(self, hive_name, root_key):
    method display_registry_keys (line 148) | def display_registry_keys(self, parent_item, registry_key):
    method display_registry_values (line 157) | def display_registry_values(self, parent_key_item, registry_key):
    method display_metadata (line 165) | def display_metadata(self, registry_object):
    method setup_table (line 184) | def setup_table(self, values):
    method display_values_in_table (line 208) | def display_values_in_table(self, values):
    method on_item_clicked (line 211) | def on_item_clicked(self, item, column):
    method clear (line 222) | def clear(self):

FILE: modules/text_tab.py
  class SearchDirection (line 17) | class SearchDirection(Enum):
  class TextViewerManager (line 22) | class TextViewerManager:
    method __init__ (line 25) | def __init__(self):
    method get_total_pages (line 36) | def get_total_pages(self):
    method detect_encoding (line 40) | def detect_encoding(file_content_chunk):
    method extract_strings_from_content (line 45) | def extract_strings_from_content(self):
    method load_text_content (line 58) | def load_text_content(self, file_content):
    method get_text_content_for_current_page (line 63) | def get_text_content_for_current_page(self):
    method change_page (line 68) | def change_page(self, delta):
    method jump_to_start (line 75) | def jump_to_start(self):
    method jump_to_end (line 80) | def jump_to_end(self):
    method search_for_string (line 85) | def search_for_string(self, search_str, direction=SearchDirection.NEXT):
    method clear_content (line 119) | def clear_content(self):
  class TextViewer (line 127) | class TextViewer(QWidget):
    method __init__ (line 128) | def __init__(self, parent=None):
    method init_ui (line 135) | def init_ui(self):
    method setup_toolbar (line 145) | def setup_toolbar(self):
    method setup_text_edit (line 227) | def setup_text_edit(self):
    method display_text_content (line 232) | def display_text_content(self, file_content):
    method clear_content (line 236) | def clear_content(self):
    method search_next (line 240) | def search_next(self):
    method update_highlighted_text (line 247) | def update_highlighted_text(self):
    method update_font_size (line 264) | def update_font_size(self):
    method go_to_page_by_entry (line 270) | def go_to_page_by_entry(self):
    method refresh_content (line 281) | def refresh_content(self):
  class CustomTextEdit (line 290) | class CustomTextEdit(QTextEdit):
    method __init__ (line 291) | def __init__(self, *args, **kwargs):
    method contextMenuEvent (line 295) | def contextMenuEvent(self, event):
    method decodeBase64 (line 315) | def decodeBase64(self):
    method decodeHex (line 318) | def decodeHex(self):
    method decodeURL (line 321) | def decodeURL(self):
    method decodeHTML (line 324) | def decodeHTML(self):
    method decodeOctal (line 327) | def decodeOctal(self):
    method decodeBinary (line 330) | def decodeBinary(self):
    method decodeSelectedText (line 333) | def decodeSelectedText(self, encoding_type):
    method getDecodedText (line 366) | def getDecodedText(self, selected_text):
    method tryDecodeBase64 (line 382) | def tryDecodeBase64(self, text):
    method tryDecodeHex (line 389) | def tryDecodeHex(self, text):
    method tryDecodeURL (line 396) | def tryDecodeURL(self, text):
    method tryDecodeHTML (line 402) | def tryDecodeHTML(self, text):
    method tryDecodeOctal (line 408) | def tryDecodeOctal(self, text):
    method tryDecodeBinary (line 414) | def tryDecodeBinary(self, text):
    method mouseMoveEvent (line 422) | def mouseMoveEvent(self, event):

FILE: modules/unified_application_manager.py
  class PyTsk3StreamDevice (line 24) | class PyTsk3StreamDevice(QIODevice):
    method __init__ (line 27) | def __init__(self, file_obj, file_size, parent=None):
    method size (line 35) | def size(self):
    method isSequential (line 39) | def isSequential(self):
    method seek (line 43) | def seek(self, pos):
    method pos (line 51) | def pos(self):
    method atEnd (line 55) | def atEnd(self):
    method readData (line 59) | def readData(self, maxSize):
    method writeData (line 89) | def writeData(self, data):
    method close (line 93) | def close(self):
  class UnifiedViewer (line 100) | class UnifiedViewer(QWidget):
    method __init__ (line 101) | def __init__(self, parent=None):
    method ensure_icons_directory (line 129) | def ensure_icons_directory(self):
    method create_default_icon (line 146) | def create_default_icon(self, path, size, color):
    method get_pdf_viewer (line 191) | def get_pdf_viewer(self):
    method get_picture_viewer (line 199) | def get_picture_viewer(self):
    method get_audio_video_player (line 207) | def get_audio_video_player(self):
    method load (line 215) | def load(self, content=None, mime_type=None, path=None, file_obj=None,...
    method clear (line 326) | def clear(self):
    method display_application_content (line 385) | def display_application_content(self, file_content, full_file_path):
    method closeEvent (line 406) | def closeEvent(self, event):
    method __del__ (line 439) | def __del__(self):
    method shutdown (line 451) | def shutdown(self):
  class PictureViewer (line 503) | class PictureViewer(QWidget):
    method __init__ (line 504) | def __init__(self, parent=None):
    method initialize_ui (line 510) | def initialize_ui(self):
    method setup_toolbar (line 543) | def setup_toolbar(self):
    method display (line 581) | def display(self, content):
    method clear (line 589) | def clear(self):
    method zoom_in (line 592) | def zoom_in(self):
    method zoom_out (line 597) | def zoom_out(self):
    method rotate_left (line 602) | def rotate_left(self):
    method rotate_right (line 607) | def rotate_right(self):
    method reset (line 612) | def reset(self):
    method export_original_image (line 616) | def export_original_image(self):
  class PDFViewer (line 633) | class PDFViewer(QWidget):
    method __init__ (line 634) | def __init__(self, parent=None):
    method initialize_ui (line 651) | def initialize_ui(self):
    method setup_toolbar (line 680) | def setup_toolbar(self):
    method setup_pdf_display_area (line 793) | def setup_pdf_display_area(self):
    method set_current_page (line 803) | def set_current_page(self, page_num):
    method go_to_page (line 813) | def go_to_page(self):
    method update_navigation_states (line 821) | def update_navigation_states(self):
    method show_previous_page (line 839) | def show_previous_page(self):
    method show_next_page (line 844) | def show_next_page(self):
    method show_page (line 849) | def show_page(self, page_num):
    method display (line 884) | def display(self, content):
    method cleanup_pdf_content (line 916) | def cleanup_pdf_content(content):
    method clear (line 958) | def clear(self):
    method show_first_page (line 969) | def show_first_page(self):
    method show_last_page (line 973) | def show_last_page(self):
    method zoom_in (line 978) | def zoom_in(self):
    method zoom_out (line 989) | def zoom_out(self):
    method set_zoom_from_entry (line 1000) | def set_zoom_from_entry(self):
    method reset_zoom (line 1021) | def reset_zoom(self):
    method fit_window (line 1029) | def fit_window(self):
    method fit_width (line 1044) | def fit_width(self):
    method rotate_left (line 1057) | def rotate_left(self):
    method rotate_right (line 1064) | def rotate_right(self):
    method toggle_pan_mode (line 1071) | def toggle_pan_mode(self, checked):
    method mousePressEvent (line 1076) | def mousePressEvent(self, event):
    method mouseMoveEvent (line 1085) | def mouseMoveEvent(self, event):
    method mouseReleaseEvent (line 1101) | def mouseReleaseEvent(self, event):
    method print_pdf (line 1108) | def print_pdf(self):
    method save_pdf (line 1155) | def save_pdf(self):
  class AudioVideoPlayer (line 1178) | class AudioVideoPlayer(QWidget):
    method __init__ (line 1179) | def __init__(self, parent=None):
    method initialize_ui (line 1198) | def initialize_ui(self):
    method set_audio_only_mode (line 1231) | def set_audio_only_mode(self, is_audio_only=True):
    method handle_media_status_change (line 1251) | def handle_media_status_change(self, status):
    method setup_connections (line 1267) | def setup_connections(self):
    method _setup_os_volume (line 1285) | def _setup_os_volume(self):
    method set_os_volume (line 1299) | def set_os_volume(self, volume_level):
    method toggle_play (line 1308) | def toggle_play(self):
    method stop (line 1314) | def stop(self):
    method update_play_state (line 1324) | def update_play_state(self, state):
    method update_controls (line 1329) | def update_controls(self):
    method set_position (line 1363) | def set_position(self, position):
    method update_position (line 1366) | def update_position(self, position):
    method update_duration (line 1375) | def update_duration(self, duration):
    method format_time (line 1379) | def format_time(self, milliseconds):
    method toggle_mute (line 1385) | def toggle_mute(self):
    method set_volume (line 1425) | def set_volume(self, volume):
    method handle_error (line 1472) | def handle_error(self, error, error_string):
    method closeEvent (line 1476) | def closeEvent(self, event):
    method __del__ (line 1485) | def __del__(self):
    method create_controls (line 1494) | def create_controls(self):
    method safe_stop (line 1594) | def safe_stop(self):

FILE: modules/verification.py
  class HashCalculationThread (line 7) | class HashCalculationThread(QThread):
    method __init__ (line 11) | def __init__(self, image_handler):
    method run (line 16) | def run(self):
    method update_progress (line 29) | def update_progress(self, current, total):
    method stop (line 39) | def stop(self):
  class VerificationWidget (line 44) | class VerificationWidget(QWidget):
    method __init__ (line 45) | def __init__(self, image_handler, parent=None):
    method closeEvent (line 127) | def closeEvent(self, event):
    method save_hash (line 140) | def save_hash(self):
    method start_hash_calculation (line 146) | def start_hash_calculation(self):
    method update_progress (line 157) | def update_progress(self, percentage):
    method on_hash_calculated (line 165) | def on_hash_calculated(self, hash_results):
    method copy_hash (line 222) | def copy_hash(self):
    method is_verified (line 227) | def is_verified(self):

FILE: modules/veriphone_api.py
  class VeriphoneWidget (line 8) | class VeriphoneWidget(QWidget):
    method __init__ (line 9) | def __init__(self):
    method init_ui (line 14) | def init_ui(self):
    method set_api_key (line 70) | def set_api_key(self, key):
    method use_api_key (line 73) | def use_api_key(self):
    method verify_phone_number (line 77) | def verify_phone_number(self):
    method update_veriphone_info (line 89) | def update_veriphone_info(self, phone_number):
    method verify_phone_with_veriphone (line 97) | def verify_phone_with_veriphone(self, phone_number):
    method format_data_as_html (line 105) | def format_data_as_html(self, data):

FILE: modules/virus_total_tab.py
  class VirusTotal (line 15) | class VirusTotal(QWidget):
    method __init__ (line 16) | def __init__(self):
    method init_ui (line 29) | def init_ui(self):
    method set_api_key (line 70) | def set_api_key(self, key):
    method use_api_key (line 73) | def use_api_key(self):
    method spacer (line 77) | def spacer(self, policy1, policy2):
    method setup_logo_toolbar (line 82) | def setup_logo_toolbar(self):
    method setup_action_toolbar (line 92) | def setup_action_toolbar(self):
    method virus_total_website (line 112) | def virus_total_website(self, event):
    method reset_ui (line 116) | def reset_ui(self):
    method set_file_hash (line 122) | def set_file_hash(self, file_hash):
    method set_file_content (line 126) | def set_file_content(self, file_content, file_name="unnamed_file"):
    method upload_file (line 134) | def upload_file(self):
    method zip_file_in_memory (line 149) | def zip_file_in_memory(self, content: bytes, file_name: str):
    method upload_file_to_virustotal (line 157) | def upload_file_to_virustotal(self, file_content, file_name):
    method process_vt_response (line 175) | def process_vt_response(self, response):
    method pass_hash (line 186) | def pass_hash(self):
    method update_virustotal_info (line 201) | def update_virustotal_info(self):
    method vt_getresult (line 213) | def vt_getresult(self, hashes):
    method format_data_as_html (line 261) | def format_data_as_html(self, data):
    method view_in_browser (line 319) | def view_in_browser(self):
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (832K chars).
[
  {
    "path": ".gitignore",
    "chars": 361,
    "preview": "# Ignore E01, dd, and raw image files in the root directory\n/*.E01\n/*.dd\n/*.raw\n\n# Ignore IDE-specific folders and files"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2024 Radoslav Gadzhovski\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "README.md",
    "chars": 9430,
    "preview": "<h1 align=\"center\">Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)</h1>\n\n<p align=\"center\">\n  TRACE is a di"
  },
  {
    "path": "install_macos_linux_WSL.sh",
    "chars": 7158,
    "preview": "#!/bin/bash\nset -e\n\n# === COLORS ===\nGREEN=\"\\033[1;32m\"\nLIGHT_GREEN=\"\\033[38;5;82m\"\nCYAN=\"\\033[1;36m\"\nMAGENTA=\"\\033[1;35"
  },
  {
    "path": "main.py",
    "chars": 204,
    "preview": "\nfrom PySide6.QtWidgets import QApplication\nfrom modules.mainwindow import MainWindow\n\n\nif __name__ == '__main__':\n    a"
  },
  {
    "path": "modules/about.py",
    "chars": 2129,
    "preview": "from PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QFont, QPalette, QColor\nfrom PySide6.QtWidgets import Q"
  },
  {
    "path": "modules/converter.py",
    "chars": 9468,
    "preview": "import os\nimport subprocess\n\nimport pyewf\nfrom PySide6.QtCore import Signal\nfrom PySide6.QtGui import QIcon\nfrom PySide6"
  },
  {
    "path": "modules/exif_tab.py",
    "chars": 4389,
    "preview": "from io import BytesIO as io_BytesIO\n\nfrom PIL import Image\nfrom PIL.ExifTags import TAGS\nfrom PySide6.QtWidgets import "
  },
  {
    "path": "modules/file_carving.py",
    "chars": 47697,
    "preview": "import datetime\nimport io\nimport os\nimport struct\nimport time\nimport zipfile\nfrom concurrent.futures import ThreadPoolEx"
  },
  {
    "path": "modules/hex_tab.py",
    "chars": 30204,
    "preview": "import os\nfrom functools import lru_cache\n\nfrom PySide6.QtCore import Qt, QObject, Signal, QThread, QSize\nfrom PySide6.Q"
  },
  {
    "path": "modules/mainwindow.py",
    "chars": 201137,
    "preview": "import configparser\nimport hashlib\nimport os\nimport datetime\nimport pyewf\nimport pytsk3\nimport tempfile\nimport gc\nimport"
  },
  {
    "path": "modules/metadata_tab.py",
    "chars": 7618,
    "preview": "import os\nimport datetime\nfrom PySide6.QtWidgets import QTextEdit, QSizePolicy, QWidget, QVBoxLayout\nimport hashlib\nfrom"
  },
  {
    "path": "modules/registry.py",
    "chars": 9839,
    "preview": "import os\nimport tempfile\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QIcon\nfrom PySide6.QtWidgets import Q"
  },
  {
    "path": "modules/text_tab.py",
    "chars": 15979,
    "preview": "import base64\nimport html\nimport re\nimport sqlite3\nimport urllib\nimport urllib.parse\nfrom enum import Enum\nfrom functool"
  },
  {
    "path": "modules/unified_application_manager.py",
    "chars": 64728,
    "preview": "import os\nfrom ctypes import cast, POINTER\nfrom weakref import WeakValueDictionary\nimport mimetypes\nimport platform\nimpo"
  },
  {
    "path": "modules/verification.py",
    "chars": 9735,
    "preview": "from PySide6.QtGui import QIcon, QFont\nfrom PySide6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QPushButton, QApplic"
  },
  {
    "path": "modules/veriphone_api.py",
    "chars": 5427,
    "preview": "import requests\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QIcon\nfrom PySide6.QtWidgets import (QW"
  },
  {
    "path": "modules/virus_total_tab.py",
    "chars": 13518,
    "preview": "import io\nimport zipfile\nfrom datetime import date\nfrom time import time\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtG"
  },
  {
    "path": "requirements.txt",
    "chars": 1324,
    "preview": "aiohttp==3.8.5\naiosignal==1.3.1\nasync-timeout==4.0.3\nattrs==23.1.0\nav==16.0.0\ncertifi==2023.7.22\nchardet==5.2.0\ncharset"
  },
  {
    "path": "requirements_macos_silicon.txt",
    "chars": 1257,
    "preview": "aiohttp==3.8.5\naiosignal==1.3.1\nasync-timeout==4.0.3\nattrs==23.1.0\nav==16.0.0\ncertifi==2023.7.22\ncharset-normalizer==3.3"
  },
  {
    "path": "styles/dark_theme.qss",
    "chars": 15123,
    "preview": "/* Global Widget Styles */\nQWidget {\n    font-size: 14px;\n    color: #E0E0E0;  /* Light text color */\n    background-col"
  },
  {
    "path": "styles/light_theme.qss",
    "chars": 14193,
    "preview": "/* Global Widget Styles */\nQWidget {\n    font-size: 14px;\n    color: #333333;\n    background-color: #FFFFFF;\n}\n\n/* Butto"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/Arsenal Recon - End User License Agreement.txt",
    "chars": 27814,
    "preview": "ARSENAL RECON END USER LICENSE AGREEMENT\n\nPLEASE READ THIS LICENSE AGREEMENT CAREFULLY BEFORE USING THIS SOFTWARE. BY IN"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.deps.json",
    "chars": 72195,
    "preview": "{\n  \"runtimeTarget\": {\n    \"name\": \".NETCoreApp,Version=v6.0\",\n    \"signature\": \"\"\n  },\n  \"compilationOptions\": {},\n  \"t"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.log",
    "chars": 759,
    "preview": "---------------\n2023-09-06 14:42:21\nStarting up: Application 'C:\\Users\\Radi\\Desktop\\Final_year_project\\Arsenal-Image-Mou"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.runtimeconfig.json",
    "chars": 355,
    "preview": "{\n  \"runtimeOptions\": {\n    \"tfm\": \"net6.0\",\n    \"frameworks\": [\n      {\n        \"name\": \"Microsoft.NETCore.App\",\n      "
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.runtimeconfig.json",
    "chars": 242,
    "preview": "{\n  \"runtimeOptions\": {\n    \"tfm\": \"net6.0\",\n    \"framework\": {\n      \"name\": \"Microsoft.NETCore.App\",\n      \"version\": "
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/readme.txt",
    "chars": 66037,
    "preview": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounte"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/readme_cli.txt",
    "chars": 7297,
    "preview": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounte"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/NEWS.txt",
    "chars": 91509,
    "preview": "---------------- VERSION 4.12.1 --------------\nC/C++:\n- Bug fixes from Luis Nassif and Joachim Metz\n- Added check to sto"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/README-win32.txt",
    "chars": 2324,
    "preview": "                          The Sleuth Kit\n                        Windows Executables\n\n                http://www.sleuthk"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/README.txt",
    "chars": 9085,
    "preview": "[![Build Status](https://travis-ci.org/sleuthkit/sleuthkit.svg?branch=develop)](https://travis-ci.org/sleuthkit/sleuthki"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/bin/mactime.pl",
    "chars": 27249,
    "preview": "my $VER=\"4.12.1\";\n#\n# This program is based on the 'mactime' program by Dan Farmer and\n# and the 'mac_daddy' program by "
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/licenses/IBM-LICENSE",
    "chars": 11954,
    "preview": "IBM PUBLIC LICENSE VERSION 1.0 - CORONER TOOLKIT UTILITIES\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/licenses/cpl1.0.txt",
    "chars": 11613,
    "preview": "Common Public License Version 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC\nLICENSE (\""
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the Gadzhovski/TRACE-Forensic-Toolkit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (53.0 MB), approximately 189.8k tokens, and a symbol index with 469 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!