[
  {
    "path": ".gitignore",
    "content": "# 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\n.idea/\n\n# Ignore config files\n/config.ini\n\n# Ignore the carved_files directory\n/carved_files/\n\n# Ignore virtual environment folders\n/venv/\n\n# Ignore Python cache\n__pycache__\n\n# Ignore build artifacts\n/build/\n/dist/\n*.spec\nbuild_exe.py      "
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Radoslav Gadzhovski\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)</h1>\n\n<p align=\"center\">\n  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.\n</p>\n\n<p align=\"center\">\n  <img src=\"Icons/logo_prev_ui.png\" alt=\"TRACE Logo\" width=\"400\"/>\n</p>\n\n## Navigation 🧭 \n\n- [Preview 👀](#preview-)\n- [Features 🌟](#features-)\n- [Screenshots 📸](#screenshots-)\n- [Supported Image Formats 💾](#supported-image-formats-)\n- [Tested File Systems 🗂️](#tested-file-systems-%EF%B8%8F)\n- [Cross-Platform Compatibility 🖥️💻](#cross-platform-compatibility-%EF%B8%8F)\n- [Getting Started 🚀](#getting-started-)\n  - [Prerequisites 🛠️](#prerequisites-)\n  - [Configuration ⚙️](#configuration-%EF%B8%8F)\n  - [Running the Tool ▶️](#running-the-tool-%EF%B8%8F)\n- [Built With 🧱](#built-with-)\n- [Work in Progress 🛠️](#work-in-progress-)\n- [Testing & Feedback 🧪](#testing--feedback-)\n- [Contributing 🤝](#contributing-)\n- [Socials 👨‍💻](#socials-)\n\n\n## Preview 👀 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n<p>\n  <br/>\n  <img src=\"Icons/readme/Preview_Dark.png\" alt=\"TRACE Preview\" width=\"100%\"/>\n  <br/>\n</p>\n\n<br>\n\n## Features 🌟 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n✅ ***Image Mounting**: Mount forensic disk images. (Windows only) \\\n✅ **Tree Viewer**: Navigate through the disk image structure, including partitions and files.\\\n✅ **Detailed File Analysis**: View file content in different formats, such as HEX, text, and application-specific views.\\\n✅ **EXIF Data Extraction**: Extract and display EXIF metadata from photos.\\\n✅ **Registry Viewer**: View and examine Windows registry files.\\\n✅ **Basic File Carving**: Recover deleted files from disk images.\\\n✅ **Virus Total API Integration**: Check files for malware using the Virus Total API.\\\n✅ **E01 Image Verification**: Verify the integrity of E01 disk images.\\\n✅ **Convert E01 to Raw**: Convert E01 disk images to raw format.\\\n✅ **Message Decoding**: Decode messages from base64, binary, and other encodings.\n\n<br>\n\n## Screenshots 📸 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n### Registry Browser 🗂️\n\n<p>\n  <br/>\n  <img src=\"Icons/readme/registry.png\" alt=\"Registry Browser\" width=\"90%\"/>\n  <br/>\n</p>\n\n\n### File Carving 🔪\n\n<p>\n  <br/>\n  <img src=\"Icons/readme/carving.png\" alt=\"File Carving\" width=\"90%\"/>\n  <br/>\n</p>\n\n### File Search 🔍\n<p>\n  <br/>\n  <img src=\"Icons/readme/file_search.png\" alt=\"Image Verification\" width=\"80%\"/>\n  <br/>\n</p>\n\n### Image Verification ✅\n\n<p>\n  <br/>\n  <img src=\"Icons/readme/trace_verify.png\" alt=\"Image Verification\" width=\"70%\"/>\n  <br/>\n</p>\n\n<br>\n\n\n\n## Supported Image Formats 💾 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n| Image Format                                   | Extensions             | Split   |  Unsplit |\n|------------------------------------------------|------------------------|---------|----------|\n| EnCase® Image File (EVF / Expert Witness Format)| `*.E01` `*.Ex01`       | ✔️      | ✔️       |\n| SMART/Expert Witness Image File                | `*.s01`                | ✔️      | ✔️       |\n| Single Image Unix / Linux DD / Raw             | `*.dd`, `*.img`, `*.raw` | ✔️      | ✔️       |\n| ISO Image File                                 | `*.iso`                |         | ✔️       |\n| AccessData Image File                          | `*.ad1`                | ✔️       | ✔️        |\n\n<br>\n\n## Tested File Systems 🗂️ &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n| File System | Tested |\n|-------------|--------|\n| NTFS        | ✔️     |\n| FAT32       |        |\n| exFAT       |        |\n| HFS+        |        |\n| APFS        |        |\n| EXT2,3,4    |        |\n\n<br>\n\n\n## Cross-Platform Compatibility 🍏🐧🗔  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n| Operating System                   | Screenshot                                                                                                           |\n|------------------------------------|----------------------------------------------------------------------------------------------------------------------|\n| **macOS Sonoma** 🍏                | <a href=\"Icons/readme/macos.png\"><img src=\"Icons/readme/macos.png\" alt=\"macOS Screenshot\" width=\"900\"/></a>          |\n| **Kali Linux 2024** 🐧             | <a href=\"Icons/readme/kali.png\"><img src=\"Icons/readme/kali.png\" alt=\"Kali Linux Screenshot\" width=\"900\"/></a>       |\n| **\\*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>        |\n| **Windows 10** 🗔                  | <a href=\"Icons/readme/windows10.png\"><img src=\"Icons/readme/windows10.png\" alt=\"Windows Screenshot\" width=\"900\"/></a> |\n\n\n\n## Getting Started 🚀 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n### Installation ⚙️\n\n\n#### **Windows:**\n1.  Install Python 3.11<br>\n    (⚠️ Python 3.12 is not supported)<br>\n    [👉 Download from python.org](https://www.python.org/downloads/release/python-3110/)\n\n2.  Install Microsoft C++ Build Tools<br>\n    [👉 Download Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)\n\n    During setup, ensure the following workloads are selected:\n\n    - ✅ Desktop development with C++\n    - ✅ C++ build tools\n\n3.  Create and activate a virtual environment\n\n    ```bash\n    python -m venv venv\n    venv\\Scripts\\activate\n    ```\n\n4.  Install dependencies\n\n    ```bash\n    pip install -r requirements.txt\n    ```\n\n5.  Run the tool\n\n    ```bash\n    python main.py\n    ```\n\n\n\n#### **macOS (Apple Silicon) & Linux (Ubuntu/WSL):**\n1.  Make the installation script executable:\n\n    ```bash\n    chmod +x install_macos_linux_WSL.sh\n    ```\n\n2.  Run the installation script:\n\n    ```bash\n    ./install_macos_linux_WSL.sh\n    ```\n\n    The script will:\n    - ✅ Create and activate a Python 3.11 virtual environment\n    - ✅ Detect your system (macOS or Linux)\n    - ✅ Install required system dependencies (via Homebrew or apt)\n    - ✅Install the appropriate Python packages:\n        * `requirements_macos_silicon.txt` → macOS\n        * `requirements.txt` → Linux\n    - ✅ After installation, it will automatically activate your virtual environment and notify you that it’s ready to use.\n\n3.  Run the Tool\n\n    Once the virtual environment is activated (you’ll see `(venv)` in your terminal prompt):\n\n    ```bash\n    python main.py\n    ```\n\n\n### Configuration ⚙️ \n\n**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.\n\n\n\n\n## Built With 🧱  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n- [pytsk3](https://pypi.org/project/pytsk3/) - Python bindings for the SleuthKit\n- [libewf-python](https://github.com/libyal/libewf) - Library to access the Expert Witness Compression Format (EWF)\n- [PySide6](https://pypi.org/project/PySide6/) - Used for the GUI components.\n- [Arsenal Image Mounter](https://arsenalrecon.com/products/image-mounter/) - For mounting forensic disk images.\n\n\n## Work in Progress 🧑‍🔧  &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n- **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.\n- **File Carving**: The verification of carved files needs improvement, as it may carve data fragments that are not actual files.\n- **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.\n\n\n## Contributing 🤝 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\nI welcome contributions from the community to help improve TRACE! If you're interested in contributing, here’s how you can get involved:\n\n\n1. **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.\n2. **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.\n\n\n## Socials 👨‍💻 &nbsp;&nbsp;&nbsp;&nbsp; [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)\n\n\n[![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://linkedin.com/in/radoslav-gadzhovski)\n\n<br>\n\n![Version](https://img.shields.io/badge/version-1.2.0-purple.svg)\n![License](https://img.shields.io/badge/license-MIT-green.svg)\n\n\n"
  },
  {
    "path": "install_macos_linux_WSL.sh",
    "content": "#!/bin/bash\nset -e\n\n# === COLORS ===\nGREEN=\"\\033[1;32m\"\nLIGHT_GREEN=\"\\033[38;5;82m\"\nCYAN=\"\\033[1;36m\"\nMAGENTA=\"\\033[1;35m\"\nYELLOW=\"\\033[1;33m\"\nR=\"\\033[0m\"\n\nclear\n\n# === Animated intro ===\nanimate_intro() {\n    local frames=(\"⣾\" \"⣷\" \"⣯\" \"⣟\" \"⡿\" \"⢿\" \"⣻\" \"⣽\")\n    echo -ne \"${MAGENTA}Launching TRACE Installer \"\n    for i in {1..20}; do\n        printf \"\\b%s\" \"${frames[$((i % 8))]}\"\n        sleep 0.08\n    done\n    echo -e \"${R}\\n\"\n}\n\n# === Pulsing TRACE logo (with aligned borders) ===\nprint_banner() {\n    local colors=(\"\\033[38;5;48m\" \"\\033[38;5;118m\" \"\\033[38;5;83m\" \"\\033[38;5;77m\")\n    for i in {0..3}; do\n        clear\n        echo -e \"${CYAN}┌────────────────────────────────────────────────────────────────────┐${R}\"\n        echo -e \"${CYAN}│                                                                    │${R}\"\n        echo -e \"${CYAN}│${R}           ${colors[$i]}████████╗██████╗  █████╗ ██████╗███████╗${R}                 ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}           ${colors[$i]}╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝${R}                ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}              ${colors[$i]}██║   ██████╔╝███████║██║     █████╗${R}                  ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}              ${colors[$i]}██║   ██╔══██╗██╔══██║██║     ██╔══╝${R}                  ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}              ${colors[$i]}██║   ██║  ██║██║  ██║╚██████╗███████╗${R}                ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}              ${colors[$i]}╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚══════╝${R}                ${CYAN}│${R}\"\n        echo -e \"${CYAN}│                                                                    │${R}\" \n        echo -e \"${CYAN}│${R}                ${MAGENTA}TRACE Forensic Toolkit Installer${R}                    ${CYAN}│${R}\"\n        echo -e \"${CYAN}│${R}                ${YELLOW}Compatible with macOS • Linux • WSL${R}                 ${CYAN}│${R}\"\n        echo -e \"${CYAN}└────────────────────────────────────────────────────────────────────┘${R}\\n\"\n        sleep 0.12\n    done\n}\n\n# === Run intro and banner ===\nanimate_intro\nprint_banner\n\n# === Detect OS type ===\nOS_TYPE=$(uname)\nif [[ \"$OS_TYPE\" == \"Darwin\" ]]; then\n    DETECTED_OS=\"macOS\"\nelif [[ \"$OS_TYPE\" == \"Linux\" ]]; then\n    if grep -qi \"microsoft\" /proc/version; then\n        DETECTED_OS=\"WSL\"\n    else\n        DETECTED_OS=\"Linux\"\n    fi\nelse\n    echo -e \"${RED}❌ Unsupported OS: $OS_TYPE${R}\"\n    echo \"This installer supports macOS, Linux, and WSL only.\"\n    exit 1\nfi\n\necho -e \"${CYAN}Detected operating system:${R} ${YELLOW}$DETECTED_OS${R}\\n\"\n\n# === Confirm OS or override ===\nread -p \"Proceed with $DETECTED_OS installation? (y/n or type 'macos'/'linux'/'wsl' to override): \" USER_INPUT\nUSER_INPUT=$(echo \"$USER_INPUT\" | tr '[:upper:]' '[:lower:]')\n\nif [[ \"$USER_INPUT\" == \"n\" ]]; then\n    echo -e \"${RED}Installation cancelled.${R}\"\n    exit 0\nelif [[ \"$USER_INPUT\" == \"macos\" ]]; then\n    USER_OS=\"macOS\"\nelif [[ \"$USER_INPUT\" == \"linux\" ]]; then\n    USER_OS=\"Linux\"\nelif [[ \"$USER_INPUT\" == \"wsl\" ]]; then\n    USER_OS=\"WSL\"\nelse\n    USER_OS=\"$DETECTED_OS\"\nfi\n\necho -e \"\\n${MAGENTA}➡ Proceeding with installation for:${R} ${YELLOW}$USER_OS${R}\"\necho \"------------------------------------------------------------\"\necho \"\"\n\n# ------------------------------------------------------------------------------\n# macOS INSTALLATION\n# ------------------------------------------------------------------------------\nif [[ \"$USER_OS\" == \"macOS\" ]]; then\n    echo -e \"${CYAN}🍏 Installing macOS system dependencies...${R}\"\n\n    if ! command -v brew &> /dev/null; then\n        echo -e \"${YELLOW}Homebrew not found.${R}\"\n        read -p \"Would you like to install Homebrew now? (y/n): \" install_brew\n        if [[ \"$install_brew\" == \"y\" || \"$install_brew\" == \"Y\" ]]; then\n            /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n            echo -e \"${GREEN}✅ Homebrew installed successfully.${R}\"\n        else\n            echo -e \"${RED}❌ Homebrew is required. Exiting.${R}\"\n            exit 1\n        fi\n    fi\n\n    brew update\n    brew install ffmpeg poppler libmagic\n\n    echo -e \"\\n${CYAN}📦 Creating Python virtual environment...${R}\"\n    python3 -m venv venv\n\n    echo -e \"\\n${CYAN}📥 Installing Python packages...${R}\"\n    source venv/bin/activate\n    pip install --upgrade pip setuptools wheel\n    pip install --no-cache-dir -r requirements_macos_silicon.txt\n    deactivate\n\n    echo -e \"\\n${GREEN}✅ Installation complete!${R}\"\n    echo -e \"\\nTo use ${MAGENTA}TRACE${R}, activate the environment first:\"\n    echo -e \"   ${YELLOW}source venv/bin/activate${R}\"\n    echo -e \"Then start the tool with:\"\n    echo -e \"   ${YELLOW}python main.py${R}\\n\"\n    echo \"When finished, type 'deactivate'.\"\n    exit 0\nfi\n\n# ------------------------------------------------------------------------------\n# LINUX INSTALLATION\n# ------------------------------------------------------------------------------\nif [[ \"$USER_OS\" == \"Linux\" ]]; then\n    echo -e \"${CYAN}🐧 Installing Linux system dependencies...${R}\"\n    sudo apt update\n    sudo apt install -y python3 python3-venv python3-pip libxcb-cursor0 libva-dev libva-drm2 ewf-tools\n\n    echo -e \"\\n${CYAN}📦 Creating Python virtual environment...${R}\"\n    python3 -m venv venv\n\n    echo -e \"\\n${CYAN}📥 Installing Python packages...${R}\"\n    source venv/bin/activate\n    pip install --upgrade pip setuptools wheel\n    pip install --no-cache-dir -r requirements_macos_silicon.txt\n    deactivate\n\n    echo -e \"\\n${GREEN}✅ Installation complete!${R}\"\n    echo -e \"\\nTo use ${MAGENTA}TRACE${R}, activate the environment first:\"\n    echo -e \"   ${YELLOW}source venv/bin/activate${R}\"\n    echo -e \"Then start the tool with:\"\n    echo -e \"   ${YELLOW}python main.py${R}\\n\"\n    echo \"When finished, type 'deactivate'.\"\n    exit 0\nfi\n\n# ------------------------------------------------------------------------------\n# WSL INSTALLATION\n# ------------------------------------------------------------------------------\nif [[ \"$USER_OS\" == \"WSL\" ]]; then\n    echo -e \"${CYAN}🐧 Installing WSL (Ubuntu) dependencies...${R}\"\n    sudo apt update\n    sudo apt install -y git python3-pip python3-venv libgl1-mesa-glx libxkbcommon0 libxkbcommon-x11-0 libegl1 \\\n                        libxcb-xinerama0 qt5dxcb-plugin qt5-wayland \\\n                        libqt5dbus5 libqt5widgets5 libqt5network5 libqt5gui5 libqt5core5a libqt5svg5 qtwayland5 \\\n                        nvidia-cuda-toolkit pulseaudio\n\n    echo -e \"\\n${CYAN}📦 Creating Python virtual environment...${R}\"\n    python3 -m venv venv\n\n    echo -e \"\\n${CYAN}📥 Installing Python packages...${R}\"\n    source venv/bin/activate\n    pip install --upgrade pip setuptools wheel\n    pip install --no-cache-dir -r requirements_macos_silicon.txt\n    deactivate\n\n    echo -e \"\\n${GREEN}✅ Installation complete!${R}\"\n    echo -e \"\\nTo use ${MAGENTA}TRACE${R}, activate the environment first:\"\n    echo -e \"   ${YELLOW}source venv/bin/activate${R}\"\n    echo -e \"Then start the tool with:\"\n    echo -e \"   ${YELLOW}python main.py${R}\\n\"\n    echo \"When finished, type 'deactivate'.\"\n    exit 0\nfi"
  },
  {
    "path": "main.py",
    "content": "\nfrom PySide6.QtWidgets import QApplication\nfrom modules.mainwindow import MainWindow\n\n\nif __name__ == '__main__':\n    app = QApplication([])\n\n    window = MainWindow()\n    window.show()\n    app.exec()\n\n\n"
  },
  {
    "path": "modules/about.py",
    "content": "from PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QFont, QPalette, QColor\nfrom PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout\n\n\nclass AboutDialog(QDialog):\n    def __init__(self, parent=None):\n        super(AboutDialog, self).__init__(parent)\n\n        self.setWindowTitle(\"About Trace\")\n        layout = QVBoxLayout(self)\n\n        # Load and scale the logo\n        logo = QLabel(self)\n        pixmap = QPixmap('Icons/logo.png')  # Ensure 'Icons/logo.png' is the correct path\n        # Adjust the logo size here\n        scaled_pixmap = pixmap.scaled(400, 400, Qt.KeepAspectRatio, Qt.SmoothTransformation)\n        logo.setPixmap(scaled_pixmap)\n        logo.setAlignment(Qt.AlignCenter)  # Center the logo\n        layout.addWidget(logo)\n\n        # Software information\n        title_label = QLabel(\"Trace - Toolkit for Retrieval and Analysis of Cyber Evidence\")\n        title_label.setAlignment(Qt.AlignCenter)\n        title_label.setFont(QFont('Arial', 20, QFont.Bold))  # Set the font, size, and weight\n        title_label.setPalette(QPalette(QColor('blue')))  # Set the text color\n        layout.addWidget(title_label)\n\n        version_label = QLabel(\"Version 1.0.0\")\n        version_label.setAlignment(Qt.AlignCenter)\n        layout.addWidget(version_label)\n\n        author_label = QLabel(\"Author: Radoslav Gadzhovski\")\n        author_label.setAlignment(Qt.AlignCenter)\n        layout.addWidget(author_label)\n\n        # Add a button to close the dialog\n        button_layout = QHBoxLayout()\n        button_layout.addStretch()  # Add stretchable space on the left\n        close_button = QPushButton(\"Close\")\n        close_button.setFixedSize(100, 30)  # Set the size of the button\n        close_button.clicked.connect(self.close)\n        button_layout.addWidget(close_button)  # Add the button to the layout\n        button_layout.addStretch()  # Add stretchable space on the right\n\n        # Add the QHBoxLayout to the main QVBoxLayout\n        layout.addLayout(button_layout)\n\n        self.setLayout(layout)\n        # Set the size of the dialog\n        self.setFixedSize(500, 700)\n"
  },
  {
    "path": "modules/converter.py",
    "content": "import os\nimport subprocess\n\nimport pyewf\nfrom PySide6.QtCore import Signal\nfrom PySide6.QtGui import QIcon\nfrom PySide6.QtWidgets import (\n    QLabel, QFileDialog, QComboBox,\n    QLineEdit, QHBoxLayout, QFormLayout\n)\nfrom PySide6.QtWidgets import (\n    QMainWindow, QPushButton, QWidget, QRadioButton,\n    QGroupBox, QVBoxLayout, QMessageBox, QStackedWidget\n)\n\n\n# Helper Function to List Drives (For Physical and Logical Drive Selection)\ndef list_drives():\n    if os.name == \"nt\":\n        # Using PowerShell command to list drives on Windows\n        command = [\"powershell\", \"-NoProfile\", \"Get-WmiObject Win32_DiskDrive | Select-Object Model, DeviceID\"]\n    elif os.name == \"darwin\":\n        # Using diskutil to list drives on macOS\n        command = [\"diskutil\", \"list\"]\n    else:\n        raise Exception(\"Unsupported OS\")\n\n    result = subprocess.run(command, capture_output=True, text=True)\n    if result.returncode != 0:\n        raise Exception(\"Failed to list drives\")\n    return result.stdout\n\n\nclass Main(QMainWindow):\n    def __init__(self):\n        super().__init__()\n        self.setWindowTitle(\"Convert E01 to DD/RAW\")\n        self.setGeometry(100, 100, 400, 400)\n        # set logo\n        self.setWindowIcon(QIcon('Icons/logo.png'))\n\n        self.stacked_widget = QStackedWidget()\n        self.setCentralWidget(self.stacked_widget)\n\n        self.init_ui()\n\n    def init_ui(self):\n        self.select_source_dialog = SelectSourceDialog(self)\n        self.stacked_widget.addWidget(self.select_source_dialog)\n\n        self.conversion_widget = ConversionWidget(self)\n        self.stacked_widget.addWidget(self.conversion_widget)\n\n        self.drive_selection_widget = DriveSelectionWidget(self)\n        self.stacked_widget.addWidget(self.drive_selection_widget)\n\n        self.select_source_dialog.sourceSelected.connect(self.show_specific_widget)\n        self.drive_selection_widget.backRequested.connect(self.show_select_source)\n\n    def show_specific_widget(self, widget_name):\n        if widget_name == \"conversion\":\n            self.stacked_widget.setCurrentWidget(self.conversion_widget)\n        elif widget_name == \"folder_contents\":\n            # Handle folder contents selection\n            pass  # Placeholder for actual implementation\n        elif widget_name == \"physical_drive\":\n            self.stacked_widget.setCurrentWidget(self.drive_selection_widget)\n\n        elif widget_name == \"logical_drive\":\n            # Handle logical drive selection\n            pass  # Placeholder for actual implementation\n\n    def show_select_source(self):\n        self.stacked_widget.setCurrentWidget(self.select_source_dialog)\n\n\n# New Widget for Drive Selection\nclass DriveSelectionWidget(QWidget):\n    backRequested = Signal()\n    driveSelected = Signal(str)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.init_ui()\n\n    def init_ui(self):\n        layout = QVBoxLayout(self)\n        self.drive_combo = QComboBox()\n        try:\n            for drive in list_drives().split('\\n'):\n                if drive.strip():\n                    self.drive_combo.addItem(drive.strip())\n        except Exception as e:\n            QMessageBox.critical(self, \"Error\", f\"Failed to list drives: {e}\")\n\n        layout.addWidget(QLabel(\"Select Drive:\"))\n        layout.addWidget(self.drive_combo)\n\n        select_button = QPushButton(\"Select\")\n        select_button.clicked.connect(self.on_select_clicked)\n        layout.addWidget(select_button)\n\n        back_button = QPushButton(\"Back\")\n        back_button.clicked.connect(lambda: self.backRequested.emit())\n        layout.addWidget(back_button)\n\n    def on_select_clicked(self):\n        selected_drive = self.drive_combo.currentText()\n        self.driveSelected.emit(selected_drive.split()[-1])  # Assuming the device ID is the last part\n\n\nclass SelectSourceDialog(QWidget):\n    sourceSelected = Signal(str)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.layout = QVBoxLayout(self)\n\n        self.group_box = QGroupBox(\"Select the Source Type\")\n        self.layout.addWidget(self.group_box)\n\n        self.radio_buttons_layout = QVBoxLayout()\n        # Existing options\n        self.image_file_radio = QRadioButton(\"Image File\")\n        # New source options\n        self.physical_drive_radio = QRadioButton(\"Physical Drive (not implemented)\")\n        self.logical_drive_radio = QRadioButton(\"Logical Drive (not implemented)\")\n        self.contents_of_folder_radio = QRadioButton(\"Contents of a Folder (not implemented)\")\n\n        # Add the radio buttons to the layout\n        self.radio_buttons_layout.addWidget(self.image_file_radio)\n        self.radio_buttons_layout.addWidget(self.physical_drive_radio)\n        self.radio_buttons_layout.addWidget(self.logical_drive_radio)\n        self.radio_buttons_layout.addWidget(self.contents_of_folder_radio)\n        self.group_box.setLayout(self.radio_buttons_layout)\n\n        self.next_button = QPushButton(\"Next\")\n        self.next_button.clicked.connect(self.on_next_clicked)\n        self.layout.addWidget(self.next_button)\n\n        self.close_button = QPushButton(\"Close\")\n        self.close_button.clicked.connect(lambda: self.window().close())\n        self.layout.addWidget(self.close_button)\n\n    def on_next_clicked(self):\n        if self.image_file_radio.isChecked():\n            self.sourceSelected.emit(\"conversion\")\n        elif self.contents_of_folder_radio.isChecked():\n            self.sourceSelected.emit(\"folder_contents\")\n        elif self.physical_drive_radio.isChecked():\n            self.sourceSelected.emit(\"physical_drive\")\n        elif self.logical_drive_radio.isChecked():\n            self.sourceSelected.emit(\"logical_drive\")\n\n\nclass ConversionWidget(QWidget):\n    backRequested = Signal()  # Signal to request going back to the source selection\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.setGeometry(100, 100, 400, 400)\n        self.setWindowTitle(\"Convert E01 to DD/RAW\")\n        self.setWindowIcon(QIcon('Icons/logo.png'))\n        self.init_ui()\n\n    def init_ui(self):\n        layout = QVBoxLayout(self)\n\n        form_layout = QFormLayout()\n        self.input_line_edit = QLineEdit()\n        browse_button = QPushButton(\"Browse...\")\n        browse_button.clicked.connect(self.browse_file)\n\n        self.format_combo_box = QComboBox()\n        self.format_combo_box.addItems([\"DD\", \"RAW\"])\n\n        self.output_line_edit = QLineEdit()\n        output_dir_button = QPushButton(\"Select Output Directory...\")\n        output_dir_button.clicked.connect(self.select_output_dir)\n\n        form_layout.addRow(QLabel(\"Select E01 File:\"), self.input_line_edit)\n        form_layout.addRow(browse_button)\n        form_layout.addRow(QLabel(\"Select Output Format:\"), self.format_combo_box)\n        form_layout.addRow(QLabel(\"Select Output Directory:\"), self.output_line_edit)\n        form_layout.addRow(output_dir_button)\n\n        layout.addLayout(form_layout)\n\n        # Buttons layout\n        buttons_layout = QHBoxLayout()\n        back_button = QPushButton(\"Back\")\n        back_button.clicked.connect(self.on_back_clicked)\n\n        convert_button = QPushButton(\"Convert\")\n        convert_button.clicked.connect(self.convert)\n\n        buttons_layout.addWidget(back_button)\n        buttons_layout.addWidget(convert_button)\n\n        layout.addLayout(buttons_layout)\n\n    def on_back_clicked(self):\n        main_window = self.parent().parent()\n        main_window.show_select_source()\n\n    def browse_file(self):\n        filename, _ = QFileDialog.getOpenFileName(self, \"Select E01 File\", \"\", \"E01 Files (*.e01)\")\n        if filename:\n            self.input_line_edit.setText(filename)\n\n    def select_output_dir(self):\n        directory = QFileDialog.getExistingDirectory(self, \"Select Output Directory\")\n        if directory:\n            self.output_line_edit.setText(directory)\n\n    def convert(self):\n        input_path = self.input_line_edit.text()\n        output_format = self.format_combo_box.currentText().lower()  # 'dd' or 'raw'\n        output_dir = self.output_line_edit.text()\n\n        if not input_path or not os.path.isfile(input_path):\n            QMessageBox.warning(self, \"Error\", \"The specified E01 file does not exist.\")\n            return\n\n        if not output_dir or not os.path.isdir(output_dir):\n            QMessageBox.warning(self, \"Error\", \"The specified output directory does not exist.\")\n            return\n\n        output_filename = f\"{os.path.splitext(os.path.basename(input_path))[0]}.{output_format}\"\n        output_path = os.path.join(output_dir, output_filename)\n\n        try:\n            self.perform_conversion(input_path, output_path)\n            QMessageBox.information(self, \"Success\", f\"File has been successfully converted to {output_path}\")\n        except Exception as e:\n            QMessageBox.critical(self, \"Conversion Failed\", f\"An error occurred: {str(e)}\")\n\n    def perform_conversion(self, input_path, output_path):\n        normalized_input_path = os.path.normpath(input_path)\n        filenames = pyewf.glob(normalized_input_path)\n\n        ewf_handle = pyewf.handle()\n        ewf_handle.open(filenames)\n\n        with open(output_path, 'wb') as output_file:\n            buffer_size = ewf_handle.bytes_per_sector\n            while True:\n                data = ewf_handle.read(buffer_size)\n                if not data:\n                    break\n                output_file.write(data)\n        ewf_handle.close()\n"
  },
  {
    "path": "modules/exif_tab.py",
    "content": "from io import BytesIO as io_BytesIO\n\nfrom PIL import Image\nfrom PIL.ExifTags import TAGS\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit\n\n\nclass ExifViewerManager:\n    def __init__(self):\n        self.exif_data = None\n\n    @staticmethod\n    def get_exif_data_from_content(file_content):\n        \"\"\"Extract EXIF data from the given file content.\"\"\"\n        try:\n            # Open the image from the given content\n            image = Image.open(io_BytesIO(file_content))\n\n            # Return None if the image format doesn't support EXIF\n            if image.format != \"JPEG\":\n                return None\n\n            # Return the extracted EXIF data\n            return image._getexif()\n        except Exception as e:\n            print(f\"Error extracting EXIF data: {e}\")\n            return None\n\n    def load_exif_data(self, file_content):\n        \"\"\"Load and process the EXIF data from the file content.\"\"\"\n        exif_data = self.get_exif_data_from_content(file_content)\n        structured_data = []\n\n        # If EXIF data is found, process it\n        if exif_data:\n            for key in exif_data.keys():\n                if key in TAGS and isinstance(exif_data[key], (str, bytes)):\n                    try:\n                        tag_name = TAGS[key]\n                        tag_value = exif_data[key]\n                        structured_data.append((tag_name, tag_value))\n                    except Exception as e:\n                        print(f\"Error processing key {key}: {e}\")\n            return structured_data\n        else:\n            return None\n\n\nclass ExifViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        # Initialize the manager to handle EXIF data\n        self.manager = ExifViewerManager()\n        self.init_ui()\n\n    def init_ui(self):\n        \"\"\"Initialize the user interface components.\"\"\"\n        # Set up a read-only text edit for displaying the EXIF data\n        self.text_edit = QTextEdit(self)\n        self.text_edit.setStyleSheet(\"border: 0px;\")\n        self.text_edit.setReadOnly(True)\n        self.text_edit.setContentsMargins(0, 0, 0, 0)\n\n        # Create the layout and add the text edit to it\n        layout = QVBoxLayout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.addWidget(self.text_edit)\n\n        # Set the layout for the widget\n        self.setLayout(layout)\n\n    def display_exif_data(self, exif_data):\n        \"\"\"Display the provided EXIF data in the text edit.\"\"\"\n        if exif_data:\n            # Format the EXIF data as an HTML table with CSS styling\n            exif_table = f\"\"\"\n                <style>\n                    body {{\n                        margin: 0;\n                        padding: 0;\n                        font-family: Arial, sans-serif;\n                    }}\n                    table {{\n                        width: 100%;\n                        border-collapse: collapse;\n                        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);\n                    }}\n                    td, th {{\n                        border: 1px solid #ddd;\n                        padding: 8px;\n                        word-wrap: break-word;\n                        text-align: left;\n                    }}\n                    th {{\n                        background-color: #ddd;  /* Changed color to a light gray */\n                        color: black;  /* Changed color to black */\n                    }}\n                    tr:nth-child(even) {{\n                        background-color: #f2f2f2;\n                    }}\n                    tr:hover {{\n                        background-color: #ddd;\n                    }}\n                </style>\n                <table>\n            \"\"\"\n            for key, value in exif_data:\n                exif_table += f\"<tr><td><b>{key}</b></td><td>{value}</td></tr>\"\n            exif_table += \"</table>\"\n            self.text_edit.setHtml(exif_table)\n        else:\n            # Clear the text edit if there's no EXIF data to display\n            self.text_edit.clear()\n\n    def clear_content(self):\n        \"\"\"Clear the displayed content.\"\"\"\n        self.text_edit.clear()\n\n    def load_and_display_exif_data(self, file_content):\n        \"\"\"Load the EXIF data from the file content and display it.\"\"\"\n        exif_data = self.manager.load_exif_data(file_content)\n        self.display_exif_data(exif_data)\n"
  },
  {
    "path": "modules/file_carving.py",
    "content": "import datetime\nimport io\nimport os\nimport struct\nimport time\nimport zipfile\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport cv2\nfrom PIL import Image, UnidentifiedImageError\nfrom PIL.ExifTags import TAGS\nfrom PyPDF2 import PdfReader\nfrom PyPDF2.errors import PdfReadError\nfrom PySide6.QtCore import QSize, QUrl, QRectF\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtCore import Signal, Slot\nfrom PySide6.QtGui import QIcon, QAction, QDesktopServices, QPixmap, QPainter, QImage\nfrom PySide6.QtSvg import QSvgRenderer\nfrom PySide6.QtWidgets import QListWidget, QListWidgetItem, QToolBar, QSizePolicy, QHBoxLayout, \\\n    QCheckBox, QHeaderView\nfrom PySide6.QtWidgets import QMenu\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QTabWidget\nfrom moviepy.editor import VideoFileClip\nfrom pdf2image import convert_from_path\n\n\nclass NumericTableWidgetItem(QTableWidgetItem):\n    def __lt__(self, other):\n        self_value = self.text().split()[0]  # Extract numeric part of the text\n        other_value = other.text().split()[0]  # Extract numeric part of the text\n        self_unit = self.text().split()[1]  # Extract unit part of the text\n        other_unit = other.text().split()[1]  # Extract unit part of the text\n        units = {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}\n\n        # Convert to bytes for comparison\n        self_bytes = float(self_value) * (1024 ** units[self_unit])\n        other_bytes = float(other_value) * (1024 ** units[other_unit])\n\n        return self_bytes < other_bytes\n\n\nclass FileCarvingWidget(QWidget):\n    file_carved = Signal(str, str, str, str, str)  # Unified signal for file carving\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.main_window = parent  # Store reference to MainWindow before it gets reparented by tab widget\n        self.image_handler = None\n        self.executor = ThreadPoolExecutor(max_workers=4)  # ThreadPoolExecutor for background tasks\n        self.carved_files = []\n        self.carved_file_names = set()  # Track carved file names to avoid duplicates\n        self.allocation_map = []  # Map of allocated disk regions to skip during carving\n        self.init_ui()\n\n    def init_ui(self):\n        self.layout = QVBoxLayout(self)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)  # Set the spacing to zero\n\n        self.toolbar = QToolBar()\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.layout.addWidget(self.toolbar)\n\n        self.icon_label = QLabel()\n        self.icon_label.setPixmap(QPixmap('Icons/icons8-carving-64.png'))\n        self.icon_label.setFixedSize(48, 48)\n        self.toolbar.addWidget(self.icon_label)\n\n        self.title_label = QLabel(\"File Carving\")\n        self.title_label.setStyleSheet(\"\"\"\n            QLabel {\n                font-size: 20px; /* Slightly larger size for the title */\n                color: #37c6d0; /* Hex color for the text */\n                font-weight: bold; /* Make the text bold */\n                margin-left: 8px; /* Space between icon and label */\n            }\n        \"\"\")\n        self.toolbar.addWidget(self.title_label)\n\n        self.spacer = QLabel()\n        self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(self.spacer)\n\n        self.table_widget = self.create_table_widget()\n        self.table_widget.resizeEvent = self.handle_resize_event\n\n        self.list_widget = self.create_list_widget()\n\n        self.fileTypeLayout = QHBoxLayout()\n        self.fileTypes = {\"All\": QCheckBox(\"All\"), \"PDF\": QCheckBox(\"PDF\"), \"JPG\": QCheckBox(\"JPG\"),\n                          \"PNG\": QCheckBox(\"PNG\"), \"GIF\": QCheckBox(\"GIF\"), \"WAV\": QCheckBox(\"WAV\"),\n                          \"MOV\": QCheckBox(\"MOV\"), \"WMV\": QCheckBox(\"WMV\"), \"ZIP\": QCheckBox(\"ZIP\"),\n                          'BMP': QCheckBox(\"BMP\")}\n\n        for fileType, checkBox in self.fileTypes.items():\n            self.fileTypeLayout.addWidget(checkBox)\n\n        # Adding a widget to contain the file type checkboxes\n        self.fileTypeWidget = QWidget()\n        self.fileTypeWidget.setLayout(self.fileTypeLayout)\n        self.toolbar.addWidget(self.fileTypeWidget)\n\n        self.start_button = QPushButton(\"Start\")\n        self.start_button.clicked.connect(self.start_carving)\n\n        self.toolbar.addWidget(self.start_button)\n\n        self.stop_button = QPushButton(\"Stop\")\n        self.stop_button.clicked.connect(self.stop_carving)\n        self.stop_button.setEnabled(False)\n\n        self.toolbar.addWidget(self.stop_button)\n        self.layout.addWidget(self.tab_widget)\n\n        self.file_carved.connect(self.display_carved_file)\n\n    def create_table_widget(self):\n        table_widget = QTableWidget()\n        table_widget.setColumnCount(6)  # Id, Name, Size, Type, Modification Date, File Path\n        table_widget.setSelectionBehavior(QTableWidget.SelectRows)\n        table_widget.setEditTriggers(QTableWidget.NoEditTriggers)\n        table_widget.setSortingEnabled(True)\n        table_widget.verticalHeader().setVisible(False)\n        table_widget.setObjectName(\"fileCarvingTable\")  # For CSS styling\n\n        # Set size policy to expand with window\n        table_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n        # Use alternate row colors (matching Listing tab)\n        table_widget.setAlternatingRowColors(True)\n        table_widget.setIconSize(QSize(24, 24))\n\n        # Enable horizontal scrolling for smaller windows (matching Listing tab)\n        table_widget.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)\n        table_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n\n        # Configure header - all columns use Interactive mode for horizontal scrolling\n        header = table_widget.horizontalHeader()\n        header.setSectionResizeMode(0, QHeaderView.Interactive)  # Id - fixed, manually resizable\n        header.setSectionResizeMode(1, QHeaderView.Interactive)  # Name - fixed, manually resizable\n        header.setSectionResizeMode(2, QHeaderView.Interactive)  # Size - fixed, manually resizable\n        header.setSectionResizeMode(3, QHeaderView.Interactive)  # Type - fixed, manually resizable\n        header.setSectionResizeMode(4, QHeaderView.Interactive)  # Modification Date - fixed, manually resizable\n        header.setSectionResizeMode(5, QHeaderView.Interactive)  # File Path - fixed, manually resizable\n\n        # Set column widths (matching Listing tab style)\n        table_widget.setColumnWidth(0, 100)   # Id - compact\n        table_widget.setColumnWidth(1, 400)  # Name - widest (matching Listing tab)\n        table_widget.setColumnWidth(2, 100)   # Size - compact (matching Listing tab)\n        table_widget.setColumnWidth(3, 100)   # Type - compact (matching Listing tab)\n        table_widget.setColumnWidth(4, 160)   # Modification Date - matching timestamp columns in Listing\n        table_widget.setColumnWidth(5, 1100)  # File Path - wide (matching Listing tab)\n\n        # Set header alignment (matching Listing tab)\n        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)\n\n        # Set the header labels\n        table_widget.setHorizontalHeaderLabels(['Id', 'Name', 'Size', 'Type', 'Modification Date', 'File Path'])\n\n        # Context menu and click handlers\n        table_widget.setContextMenuPolicy(Qt.CustomContextMenu)\n        table_widget.customContextMenuRequested.connect(self.open_context_menu)\n        table_widget.cellClicked.connect(self.on_carved_file_clicked)\n\n        self.tab_widget = QTabWidget()\n        self.tab_widget.addTab(table_widget, \"File List\")\n        return table_widget\n\n    def create_list_widget(self):\n        list_widget = QListWidget()\n        list_widget.setViewMode(QListWidget.IconMode)\n        list_widget.setIconSize(QSize(120, 120))\n        list_widget.setResizeMode(QListWidget.Adjust)\n        list_widget.setUniformItemSizes(True)\n        list_widget.setSpacing(5)\n        list_widget.setContextMenuPolicy(Qt.CustomContextMenu)\n        list_widget.customContextMenuRequested.connect(self.open_context_menu)\n        # Connect click event to open file in internal viewer\n        list_widget.itemClicked.connect(self.on_carved_file_clicked)\n\n        toolbar = QToolBar()\n\n        # Define actions\n        action_small_size = (QAction(\"Small Size\", self))\n        action_small_size.setIcon(QIcon('Icons/icons8-small-icons-50.png'))\n\n        action_medium_size = (QAction(\"Medium Size\", self))\n        action_medium_size.setIcon(QIcon('Icons/icons8-medium-icons-50.png'))\n\n        action_large_size = (QAction(\"Large Size\", self))\n        action_large_size.setIcon(QIcon('Icons/icons8-large-icons-50.png'))\n\n        # Set icons\n\n        # Connect actions to new slot methods\n        action_small_size.triggered.connect(self.set_small_size)\n        action_medium_size.triggered.connect(self.set_medium_size)\n        action_large_size.triggered.connect(self.set_large_size)\n\n        # Add actions to the toolbar\n        toolbar.addAction(action_small_size)\n        toolbar.addAction(action_medium_size)\n        toolbar.addAction(action_large_size)\n\n        # Create a layout and add the toolbar and the list widget to it\n        layout = QVBoxLayout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n        layout.addWidget(toolbar)\n        layout.addWidget(list_widget)\n\n        # Create a new widget, set its layout and add it to the tab widget\n        widget = QWidget()\n        widget.setLayout(layout)\n        self.tab_widget.addTab(widget, \"Thumbnails\")\n        return list_widget\n\n    @staticmethod\n    def center_crop_to_square(pixmap, target_size):\n        \"\"\"Crop pixmap to center square and scale to target size for uniform thumbnails.\"\"\"\n        if pixmap.isNull():\n            return pixmap\n\n        width = pixmap.width()\n        height = pixmap.height()\n\n        if width == height:\n            # Already square, just scale\n            return pixmap.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)\n\n        # Determine the crop size (smaller dimension)\n        crop_size = min(width, height)\n\n        # Calculate crop position to center the crop\n        x = (width - crop_size) // 2\n        y = (height - crop_size) // 2\n\n        # Crop to square\n        cropped = pixmap.copy(x, y, crop_size, crop_size)\n\n        # Scale to target size\n        return cropped.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)\n\n    @staticmethod\n    def render_svg_to_pixmap(svg_path, target_size):\n        \"\"\"Render SVG file at target resolution for crisp icons.\"\"\"\n        renderer = QSvgRenderer(svg_path)\n        if not renderer.isValid():\n            return QPixmap()\n\n        # Create QImage at target size with transparency\n        image = QImage(target_size, target_size, QImage.Format_ARGB32)\n        image.fill(Qt.transparent)\n\n        # Render SVG onto the image\n        painter = QPainter(image)\n        renderer.render(painter, QRectF(0, 0, target_size, target_size))\n        painter.end()\n\n        # Convert QImage to QPixmap\n        return QPixmap.fromImage(image)\n\n    def set_icon_size(self, size):\n        self.list_widget.setIconSize(QSize(size, size))\n        for index in range(self.list_widget.count()):\n            item = self.list_widget.item(index)\n            item.setSizeHint(QSize(size + 10, size + 25))  # Compact padding with space for text\n\n    def set_small_size(self):\n        self.set_icon_size(80)\n\n    def set_medium_size(self):\n        self.set_icon_size(120)\n\n    def set_large_size(self):\n        self.set_icon_size(180)\n\n    def start_carving(self):\n        self.start_button.setEnabled(False)\n        self.stop_button.setEnabled(True)\n        self.clear_ui()\n        self.carved_files.clear()\n        self.carved_file_names.clear()\n\n        # Ensure the 'carved_files' and 'thumbnails' directories exist\n        if not os.path.exists(\"carved_files\"):\n            os.makedirs(\"carved_files\")\n        thumbnail_folder = os.path.join(\"carved_files\", \"thumbnails\")\n        if not os.path.exists(thumbnail_folder):\n            os.makedirs(thumbnail_folder)\n\n        # Build allocation map for all partitions to skip allocated files\n        print(\"Building allocation map for allocated files...\")\n        self.allocation_map = []\n\n        try:\n            partitions = self.image_handler.get_partitions()\n\n            if partitions:\n                # Process each partition\n                for partition_info in partitions:\n                    # partition_info is (addr, desc, start, len)\n                    start_offset = partition_info[2]  # start offset in sectors\n\n                    # Build allocation map for this partition\n                    partition_map = self.image_handler.build_allocation_map(start_offset)\n                    self.allocation_map.extend(partition_map)\n                    print(f\"  Partition at offset {start_offset}: {len(partition_map)} allocated regions\")\n            else:\n                # No partitions, try offset 0 (single filesystem)\n                if self.image_handler.has_filesystem(0):\n                    partition_map = self.image_handler.build_allocation_map(0)\n                    self.allocation_map.extend(partition_map)\n                    print(f\"  Single filesystem: {len(partition_map)} allocated regions\")\n\n            # Sort the combined allocation map\n            self.allocation_map.sort(key=lambda x: x[0])\n            print(f\"Total allocated regions to skip: {len(self.allocation_map)}\")\n\n        except Exception as e:\n            print(f\"Warning: Could not build allocation map: {e}\")\n            print(\"Will carve from entire disk (may include duplicates)\")\n            self.allocation_map = []\n\n        selected_file_types = [fileType.lower() for fileType, checkbox in self.fileTypes.items() if\n                               checkbox.isChecked()]\n        self.executor.submit(self.carve_files, selected_file_types)\n\n    def stop_carving(self):\n        self.executor.shutdown(wait=True)  # Properly shutdown the executor\n        self.start_button.setEnabled(True)  # Re-enable the start button\n        self.stop_button.setEnabled(False)  # Disable the stop button\n\n    def set_image_handler(self, image_handler):\n        self.image_handler = image_handler\n        self.start_button.setEnabled(True)\n\n    @staticmethod\n    def is_offset_allocated(offset, chunk_size, allocation_map):\n        \"\"\"\n        Check if a given offset range overlaps with any allocated regions.\"\"\"\n        if not allocation_map:\n            return False\n\n        chunk_end = offset + chunk_size\n\n        # Binary search to find potential overlapping regions\n        # We need to check if our chunk [offset, chunk_end) overlaps with any allocated region\n        left, right = 0, len(allocation_map)\n\n        while left < right:\n            mid = (left + right) // 2\n            alloc_start, alloc_end = allocation_map[mid]\n\n            # Check for overlap: two ranges overlap if one starts before the other ends\n            if offset < alloc_end and chunk_end > alloc_start:\n                return True\n\n            # If our chunk is entirely before this allocated region, search left half\n            if chunk_end <= alloc_start:\n                right = mid\n            # If our chunk is entirely after this allocated region, search right half\n            else:\n                left = mid + 1\n\n        return False\n\n    def open_context_menu(self, position):\n        menu = QMenu()\n\n        open_location_action = QAction(\"Open File Location\")\n        open_location_action.triggered.connect(self.open_file_location)\n\n        open_image_action = QAction(\"Open Externally\")\n        open_image_action.triggered.connect(self.open_image)\n\n        menu.addAction(open_location_action)\n        menu.addAction(open_image_action)\n        menu.exec_(self.table_widget.viewport().mapToGlobal(position))\n\n    def open_image(self):\n        if self.tab_widget.currentIndex() == 0:  # If the table tab is active\n            current_item = self.table_widget.currentItem()\n        else:  # If the thumbnail tab is active\n            current_item = self.list_widget.currentItem()\n\n        if current_item:\n            file_name = current_item.text()\n            for file_info in self.carved_files:\n                if file_info[0] == file_name:\n                    file_path = file_info[3]  # The file path is now at index 3\n                    QDesktopServices.openUrl(QUrl.fromLocalFile(file_path))\n                    break\n\n    def open_file_location(self):\n        current_item = self.list_widget.currentItem()\n        if current_item:\n            file_name = current_item.text()\n            for file_info in self.carved_files:\n                if file_info[0] == file_name:\n                    file_path = file_info[3]  # The file path is now at index 3\n                    QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(file_path)))\n                    break\n\n    def get_carved_timestamp(self, file_name):\n        \"\"\"Get the preserved timestamp for a carved file.\"\"\"\n        for file_info in self.carved_files:\n            if file_info[0] == file_name:\n                return file_info[4]  # Modification date at index 4\n        return None\n\n    def on_carved_file_clicked(self, *args):\n        \"\"\"Handle click on carved file to display in internal viewer.\n\n        Reads file content directly from disk image (forensically sound) instead of\n        from the carved file on disk. This ensures we're analyzing the original data.\n        \"\"\"\n        # Get clicked file name (works for both table cellClicked and list itemClicked)\n        if len(args) == 2:  # cellClicked(row, column) from table\n            row = args[0]\n            # Get file name from column 1 (Name column, column 0 is Id)\n            file_name_item = self.table_widget.item(row, 1)\n            if not file_name_item:\n                return\n            file_name = file_name_item.text()\n        elif len(args) == 1:  # itemClicked(item) from list\n            item = args[0]\n            file_name = item.text()\n        else:\n            return\n\n        # Find file info in carved_files list\n        for file_info in self.carved_files:\n            if file_info[0] == file_name:\n                file_size_str = file_info[1]  # Size at index 1\n                file_type = file_info[2]  # Type at index 2\n                file_size = int(file_size_str)\n\n                try:\n                    # Extract disk offset from filename (hex format without extension)\n                    offset_hex = os.path.splitext(file_name)[0]\n                    offset = int(offset_hex, 16)\n\n                    # Read file content directly from disk image (forensically sound!)\n                    if not self.image_handler:\n                        print(\"No image handler available\")\n                        return\n\n                    file_content = self.image_handler.read(offset, file_size)\n                    if not file_content:\n                        print(f\"Unable to read content from offset {hex(offset)}\")\n                        return\n\n                    # Create data dict for viewer (matches mainwindow's format)\n                    data = {\n                        'name': file_name,\n                        'size': file_size,\n                        'type': file_type,\n                        'offset': offset,\n                        'is_carved': True,  # Flag indicating this is a carved file\n                        'source': 'carved_file',\n                        'file_content': file_content,  # Include content so metadata viewer doesn't re-read\n                        'carved_timestamp': self.get_carved_timestamp(file_name)  # Get original timestamp if available\n                    }\n\n                    # Get MainWindow and call update_viewer_with_file_content\n                    if self.main_window and hasattr(self.main_window, 'update_viewer_with_file_content'):\n                        self.main_window.update_viewer_with_file_content(file_content, data)\n                    else:\n                        print(\"MainWindow not found or missing update_viewer_with_file_content method\")\n\n                except Exception as e:\n                    print(f\"Error opening carved file in viewer: {e}\")\n                    import traceback\n                    traceback.print_exc()\n\n                break\n\n    def setup_buttons(self):\n        self.start_button.setEnabled(False)\n        self.start_button.clicked.connect(self.start_carving_thread)\n        self.stop_button.setEnabled(False)\n        self.stop_button.clicked.connect(self.stop_carving_thread)\n\n    def start_carving_thread(self):\n        self.start_button.setEnabled(False)\n        self.stop_button.setEnabled(True)\n        # Launch carving in a background thread\n        self.executor.submit(self.carve_files)\n\n    def stop_carving_thread(self):\n        self.executor.shutdown(wait=False)\n        self.start_button.setEnabled(True)\n        self.stop_button.setEnabled(False)\n\n    def is_valid_file(self, data, file_type):\n        try:\n            if file_type == 'pdf':\n                # Validate PDF by trying to read it with PyPDF2\n                PdfReader(io.BytesIO(data))\n            elif file_type in ['jpg', 'jpeg', 'png', 'gif']:\n                # Validate images by attempting to open them with PIL\n                image = Image.open(io.BytesIO(data))\n                image.verify()  # This will not load the image but only parse it\n            elif file_type == 'bmp':\n                return True\n            elif file_type == 'wav':\n                # Basic WAV validation could check for the RIFF header, file size, etc.\n                if not data.startswith(b'RIFF') or not b'WAVE' in data[:12]:\n                    return False\n                # Additional WAV format checks could be implemented here\n            elif file_type == 'mov':\n                return True  # For now, we'll assume all MOV files are valid\n            else:\n                return True\n            return True\n        except (IOError, UnidentifiedImageError, PdfReadError, ValueError) as e:\n            print(f\"Error validating file of type {file_type}: {str(e)}\")\n            return False\n\n    def carve_pdf_files(self, chunk, global_offset):\n        pdf_start_signature = b'%PDF-'\n        pdf_linearization_signature = b'/Linearized'\n        pdf_end_signature = b'%%EOF'\n        offset = 0\n        while offset < len(chunk):\n            start_index = chunk.find(pdf_start_signature, offset)\n            if start_index == -1:\n                break\n            linearization_index = chunk.find(pdf_linearization_signature, start_index, start_index + 1024)\n            if linearization_index != -1:\n                file_size_start = chunk.find(b'/L ', linearization_index, linearization_index + 1024) + 3\n                file_size_end = chunk.find(b'/', file_size_start)\n                if file_size_end == -1:\n                    file_size_end = chunk.find(b' ', file_size_start)\n                if file_size_end != -1:\n                    try:\n                        file_size = int(chunk[file_size_start:file_size_end].split()[0])\n                        pdf_content = chunk[start_index:start_index + file_size]\n                        if self.is_valid_file(pdf_content, 'pdf'):\n                            self.save_file(pdf_content, 'pdf', global_offset + start_index, file_size)\n                            offset = start_index + file_size\n                            continue\n                    except ValueError:\n                        pass\n            end_index = chunk.find(pdf_end_signature, start_index)\n            if end_index != -1:\n                end_index += len(pdf_end_signature)\n                pdf_content = chunk[start_index:end_index]\n                if self.is_valid_file(pdf_content, 'pdf'):\n                    self.save_file(pdf_content, 'pdf', global_offset + start_index, end_index - start_index)\n                offset = end_index\n            else:\n                offset = start_index + 1\n\n    def carve_wav_files(self, chunk, offset):\n        wav_start_signature = b'RIFF'\n        offset = 0\n        while offset < len(chunk):\n            start_index = chunk.find(wav_start_signature, offset)\n            if start_index == -1:\n                break\n\n            if chunk[start_index + 8:start_index + 12] != b'WAVE':\n                offset = start_index + 4\n                continue\n\n            file_size_bytes = chunk[start_index + 4:start_index + 8]\n            file_size = int.from_bytes(file_size_bytes, byteorder='little') + 8\n\n            if start_index + file_size > len(chunk):\n                wav_content = chunk[start_index:]\n                offset = len(chunk)\n            else:\n                wav_content = chunk[start_index:start_index + file_size]\n                offset = start_index + file_size\n\n            if self.is_valid_file(wav_content, 'wav'):\n                self.save_file(wav_content, 'wav', 'carved_files', start_index)\n\n    def carve_mov_files(self, chunk, offset):\n        mov_signatures = [\n            # b'ftyp', b'moov', b'mdat', #b'pnot', b'udta', #b'uuid',\n            # b'moof', b'free', b'skip', b'jP2 ', b'wide', b'load',\n            # b'ctab', b'imap', b'matt', b'kmat', b'clip', b'crgn',\n            # b'sync', b'chap', b'tmcd', b'scpt', b'ssrc', b'PICT'\n            b'moov', b'mdat', b'free', b'wide'\n        ]\n\n        mov_file_found = False\n        mov_data = b''\n        mov_file_offset = offset\n        mov_file_size = 0\n\n        while offset < len(chunk):\n            if offset + 8 > len(chunk):\n                # Not enough data for an atom header\n                break\n\n            atom_size = int.from_bytes(chunk[offset:offset + 4], 'big')\n            atom_type = chunk[offset + 4:offset + 8]\n\n            if atom_type not in mov_signatures:\n                if mov_file_found:\n                    # End of MOV file\n                    break\n                else:\n                    # Not a MOV file or just a stray header, skip ahead\n                    offset += 4\n                    continue\n\n            mov_file_found = True\n            mov_file_size += atom_size\n\n            if offset + atom_size > len(chunk):\n                # Atom extends beyond this chunk, store what we have and wait for more data\n                mov_data += chunk[offset:]\n                break\n            else:\n                # We have the whole atom, store it\n                mov_data += chunk[offset:offset + atom_size]\n\n            offset += atom_size\n\n        if mov_file_found and mov_data:\n            # file_name = f\"carved_{mov_file_offset}.mov\"\n            # file_path = os.path.join(\"carved_files\", file_name)\n            # self.save_file(mov_data, 'mov', file_path)\n            self.save_file(mov_data, 'mov', 'carved_files', mov_file_offset)\n\n    def carve_jpg_files(self, chunk, offset):\n        jpg_start_signature = b'\\xFF\\xD8\\xFF'\n        jpg_end_signature = b'\\xFF\\xD9'\n        offset = 0\n        while offset < len(chunk):\n            start_index = chunk.find(jpg_start_signature, offset)\n            if start_index == -1:\n                break\n\n            end_index = chunk.find(jpg_end_signature, start_index)\n            if end_index != -1:\n                jpg_content = chunk[start_index:end_index + len(jpg_end_signature)]\n\n                # Check if it's a valid JPG file\n                if self.is_valid_file(jpg_content, 'jpg'):\n                    self.save_file(jpg_content, 'jpg', 'carved_files', start_index)\n\n                offset = end_index + len(jpg_end_signature)\n            else:\n                offset = start_index + 1  # Continue searching\n\n    def carve_gif_files(self, chunk, offset):\n        gif_start_signature = b'\\x47\\x49\\x46\\x38'\n        gif_end_signature = b'\\x00\\x3B'\n        offset = 0\n        while offset < len(chunk):\n            start_index = chunk.find(gif_start_signature, offset)\n            if start_index == -1:\n                break\n\n            end_index = chunk.find(gif_end_signature, start_index)\n            if end_index != -1:\n                gif_content = chunk[start_index:end_index + len(gif_end_signature)]\n\n                # Check if it's a valid GIF file\n                if self.is_valid_file(gif_content, 'gif'):\n                    self.save_file(gif_content, 'gif', 'carved_files', start_index)\n\n                offset = end_index + len(gif_end_signature)\n            else:\n                offset = start_index + 1\n\n    def carve_png_files(self, chunk, offset):\n        png_start_signature = b'\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A'\n        png_end_signature = b'\\x49\\x45\\x4E\\x44\\xAE\\x42\\x60\\x82'\n        offset = 0\n        while offset < len(chunk):\n            start_index = chunk.find(png_start_signature, offset)\n            if start_index == -1:\n                break\n\n            end_index = chunk.find(png_end_signature, start_index)\n            if end_index != -1:\n                png_content = chunk[start_index:end_index + len(png_end_signature)]\n\n                # Check if it's a valid PNG file\n                if self.is_valid_file(png_content, 'png'):\n                    self.save_file(png_content, 'png', 'carved_files', start_index)\n\n                offset = end_index + len(png_end_signature)\n            else:\n                offset = start_index + 1\n\n    def carve_wmv_files(self, chunk, offset):\n        # Define ASF header signature\n        asf_header_signature = b'\\x30\\x26\\xB2\\x75\\x8E\\x66\\xCF\\x11\\xA6\\xD9\\x00\\xAA\\x00\\x62\\xCE\\x6C'\n\n        current_offset = 0\n\n        while current_offset < len(chunk):\n            # Search for ASF header\n            start_index = chunk.find(asf_header_signature, current_offset)\n            if start_index == -1:\n                break\n\n            # Find the file properties object header within the first 512 bytes of the file\n            max_search_size = min(start_index + 512, len(chunk))\n            file_properties_header = b'\\xA1\\xDC\\xAB\\x8C\\x47\\xA9\\xCF\\x11\\x8E\\xE4\\x00\\xC0\\x0C\\x20\\x53\\x65'\n            file_properties_index = chunk.find(file_properties_header, start_index, max_search_size)\n            if file_properties_index == -1:\n                current_offset = start_index + 1\n                continue\n\n            # Extract the file size located at offset 40 within the object\n            file_size_offset = file_properties_index + 40\n            file_size_bytes = chunk[file_size_offset:file_size_offset + 8]\n            file_size = int.from_bytes(file_size_bytes, byteorder='little')\n\n            # Calculate end index based on file size\n            end_index = start_index + file_size\n\n            # Extract WMV content\n            wmv_content = chunk[start_index:end_index]\n\n            # Save the WMV content directly into the carved_files directory\n            self.save_file(wmv_content, 'wmv', 'carved_files', start_index + offset)\n            current_offset = end_index\n\n    def carve_zip_files(self, chunk, global_offset):\n        # Define ZIP header signatures\n        local_file_header_signature = b'\\x50\\x4b\\x03\\x04'\n        end_of_central_dir_signature = b'\\x50\\x4b\\x05\\x06'\n\n        current_pos = 0\n        zip_file_parts = []  # List to hold all parts of the ZIP file\n\n        while current_pos < len(chunk):\n            # Search for local file header\n            local_header_index = chunk.find(local_file_header_signature, current_pos)\n            if local_header_index == -1:\n                break\n\n            # Extract compressed size from local file header\n            compressed_size = struct.unpack(\"<I\", chunk[local_header_index + 18:local_header_index + 22])[0]\n\n            # Calculate next local file header index\n            next_local_header_index = local_header_index + 30 + compressed_size\n\n            # Extract file content\n            file_content = chunk[local_header_index:next_local_header_index]\n            zip_file_parts.append(file_content)  # Add the file content to the ZIP parts list\n\n            # Move to next local file header\n            current_pos = next_local_header_index\n\n        # Now, find and append the Central Directory and End of Central Directory Record\n        end_central_dir_index = chunk.find(end_of_central_dir_signature, current_pos)\n        if end_central_dir_index != -1:\n            # Extract comment length and calculate the total end of the ZIP file structure\n            comment_length = struct.unpack(\"<H\", chunk[end_central_dir_index + 20:end_central_dir_index + 22])[0]\n            zip_end = end_central_dir_index + 22 + comment_length\n\n            # Extract the Central Directory and End of Central Directory Record\n            zip_file_structure = chunk[current_pos:zip_end]\n            zip_file_parts.append(zip_file_structure)  # Add this to the ZIP parts list\n\n        # Combine all parts into a single ZIP file content\n        if zip_file_parts:\n            complete_zip_file_content = b''.join(zip_file_parts)\n            self.save_file(complete_zip_file_content, 'zip', 'carved_files', global_offset)\n\n        return None\n\n    def carve_bmp_files(self, chunk, offset):\n        bmp_start_signature = b'BM'  # BMP files start with 'BM'\n        header_size = 14  # The static header size for BMP files\n\n        current_offset = 0\n        while current_offset < len(chunk) - header_size:\n            # Look for the BMP signature\n            start_index = chunk.find(bmp_start_signature, current_offset)\n            if start_index == -1:\n                break  # No more BMP files found\n\n            # Verify there's enough chunk left to read the BMP size\n            if start_index + header_size > len(chunk) - 4:\n                break  # Not enough data for size\n\n            # Read file size directly from header\n            bmp_file_size = int.from_bytes(chunk[start_index + 2:start_index + 6], byteorder='little')\n\n            # Sanity check for BMP size (adjust max and min size as per your need)\n            if bmp_file_size < 100 or bmp_file_size > 5000000:\n                current_offset = start_index + 2\n                continue  # Not a valid BMP size, skip to next possible start\n\n            # Read and check dimensions for further validation\n            bmp_width = int.from_bytes(chunk[start_index + 18:start_index + 22], byteorder='little')\n            bmp_height = int.from_bytes(chunk[start_index + 22:start_index + 26], byteorder='little')\n\n            # Reasonable dimensions check (adjust max width/height as per your need)\n            if bmp_width <= 0 or bmp_width > 10000 or bmp_height <= 0 or bmp_height > 10000:\n                current_offset = start_index + 2\n                continue  # Unreasonable dimensions, likely not a BMP\n\n            # Extract the BMP file if it's entirely within the chunk\n            if start_index + bmp_file_size <= len(chunk):\n                bmp_content = chunk[start_index:start_index + bmp_file_size]\n                self.save_file(bmp_content, 'bmp', 'carved_files', start_index + offset)\n                current_offset = start_index + bmp_file_size  # Move past this BMP file\n            else:\n                break  # The BMP file exceeds the chunk boundary, stop processing\n\n        # Return if more data is needed or if processing is complete\n        return None\n\n    def carve_files(self, selected_file_types):\n        try:\n            self.stop_carving = False\n            chunk_size = 1024 * 1024 * 100\n            offset = 0\n            chunks_processed = 0\n            chunks_skipped = 0\n\n            while offset < self.image_handler.get_size():\n                # Check if this chunk overlaps with allocated space\n                if self.is_offset_allocated(offset, chunk_size, self.allocation_map):\n                    # Skip this chunk - it's in allocated space (existing files)\n                    chunks_skipped += 1\n                    offset += chunk_size\n                    continue\n\n                chunks_processed += 1\n\n                chunk = self.image_handler.read(offset, chunk_size)\n                if not chunk:\n                    break\n\n                if self.stop_carving:\n                    self.stop_carving = False\n                    self.start_button.setEnabled(True)\n                    self.stop_button.setEnabled(False)\n                    print(f\"Carving stopped. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks\")\n                    return\n\n                # Call the carve function for each selected file type\n                for file_type in selected_file_types:\n                    if file_type == 'all':\n                        self.carve_wav_files(chunk, offset)\n                        self.carve_mov_files(chunk, offset)\n                        self.carve_pdf_files(chunk, offset)\n                        self.carve_jpg_files(chunk, offset)\n                        self.carve_gif_files(chunk, offset)\n                        self.carve_png_files(chunk, offset)\n                        self.carve_wmv_files(chunk, offset)\n                        self.carve_zip_files(chunk, offset)\n                        self.carve_bmp_files(chunk, offset)\n                    elif file_type == 'wav':\n                        self.carve_wav_files(chunk, offset)\n                    elif file_type == 'mov':\n                        self.carve_mov_files(chunk, offset)\n                    elif file_type == 'pdf':\n                        self.carve_pdf_files(chunk, offset)\n                    elif file_type == 'jpg':\n                        self.carve_jpg_files(chunk, offset)\n                    elif file_type == 'gif':\n                        self.carve_gif_files(chunk, offset)\n                    elif file_type == 'png':\n                        self.carve_png_files(chunk, offset)\n                    elif file_type == 'wmv':\n                        self.carve_wmv_files(chunk, offset)\n                    elif file_type == 'zip':\n                        self.carve_zip_files(chunk, offset)\n                    elif file_type == 'bmp':\n                        self.carve_bmp_files(chunk, offset)\n\n                offset += chunk_size\n\n            print(f\"Carving complete. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks\")\n        finally:\n            self.start_button.setEnabled(True)\n            self.stop_button.setEnabled(False)\n\n    @staticmethod\n    def extract_original_timestamp(file_content, file_type):\n        \"\"\"Extract original file timestamp from file headers/metadata.\n\n        Returns:\n            datetime object if timestamp found, None otherwise\n        \"\"\"\n        try:\n            if file_type.lower() in ['jpg', 'jpeg', 'png']:\n                # Extract EXIF DateTimeOriginal from images\n                try:\n                    img = Image.open(io.BytesIO(file_content))\n                    exif_data = img._getexif()\n                    if exif_data:\n                        # Look for DateTimeOriginal (tag 36867) or DateTime (tag 306)\n                        for tag_id, value in exif_data.items():\n                            tag_name = TAGS.get(tag_id, tag_id)\n                            if tag_name in ['DateTimeOriginal', 'DateTime']:\n                                # Parse format: \"2024:01:15 14:30:00\"\n                                return datetime.datetime.strptime(str(value), '%Y:%m:%d %H:%M:%S')\n                except Exception:\n                    pass\n\n            elif file_type.lower() == 'pdf':\n                # Extract CreationDate from PDF metadata\n                try:\n                    pdf = PdfReader(io.BytesIO(file_content))\n                    if pdf.metadata and '/CreationDate' in pdf.metadata:\n                        date_str = pdf.metadata['/CreationDate']\n                        # PDF date format: \"D:20240115143000\"\n                        if date_str.startswith('D:'):\n                            date_str = date_str[2:16]  # Extract YYYYMMDDHHmmss\n                            return datetime.datetime.strptime(date_str, '%Y%m%d%H%M%S')\n                except Exception:\n                    pass\n\n            elif file_type.lower() == 'zip':\n                # Extract timestamp from ZIP central directory\n                try:\n                    with zipfile.ZipFile(io.BytesIO(file_content)) as zf:\n                        if zf.namelist():\n                            # Get timestamp of first file in archive\n                            first_file_info = zf.getinfo(zf.namelist()[0])\n                            return datetime.datetime(*first_file_info.date_time)\n                except Exception:\n                    pass\n\n        except Exception as e:\n            print(f\"Error extracting timestamp for {file_type}: {e}\")\n\n        return None\n\n    def save_file(self, file_content, file_type, file_path, offset):\n        # Ensure the 'carved_files' directory exists\n        if not os.path.exists(\"carved_files\"):\n            os.makedirs(\"carved_files\")\n\n        offset_hex = format(offset, 'x')\n        file_name = f\"{offset_hex}.{file_type}\"\n        file_path = os.path.join(\"carved_files\", file_name)\n\n        # Write file content to disk\n        with open(file_path, \"wb\") as f:\n            f.write(file_content)\n\n        # Try to extract original timestamp from file metadata\n        original_timestamp = self.extract_original_timestamp(file_content, file_type)\n\n        if original_timestamp:\n            # Convert datetime to timestamp (seconds since epoch)\n            timestamp = time.mktime(original_timestamp.timetuple())\n            # Set both access time and modification time to preserve original timestamp\n            os.utime(file_path, (timestamp, timestamp))\n            modification_date = original_timestamp.strftime(\"%Y-%m-%d %H:%M:%S\")\n        else:\n            # Fall back to carving time if no original timestamp found\n            modification_date = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n\n        file_size = str(len(file_content))\n        self.carved_files.append((file_name, file_size, file_type, file_path, modification_date))\n        self.file_carved.emit(file_name, file_size, file_type, modification_date, file_path)\n        self.carved_file_names.add(file_name)\n\n    @Slot(str, str, str, str, str)\n    def display_carved_file(self, name, size, type_, modification_date, file_path):\n        row = self.table_widget.rowCount()\n        readable_size = self.image_handler.get_readable_size(int(size))\n        self.table_widget.insertRow(row)\n\n        # Get file icon based on type/extension\n        extension = type_.lower() if type_ else 'unknown'\n        icon_path = self.main_window.db_manager.get_icon_path('file', extension)\n\n        # Set Id column\n        self.table_widget.setItem(row, 0, QTableWidgetItem(str(row + 1)))\n\n        # Set Name column with icon\n        name_item = QTableWidgetItem(name)\n        name_item.setIcon(QIcon(icon_path))\n        self.table_widget.setItem(row, 1, name_item)\n\n        # Set other columns\n        self.table_widget.setItem(row, 2, NumericTableWidgetItem(readable_size))\n        self.table_widget.setItem(row, 3, QTableWidgetItem(type_))\n        self.table_widget.setItem(row, 4, QTableWidgetItem(modification_date))\n        self.table_widget.setItem(row, 5, QTableWidgetItem(file_path))\n\n        # Only proceed if the file type is one of the supported formats\n        if type_.lower() in ['jpg', 'jpeg', 'png', 'gif', 'mov', 'pdf', 'wmv', 'bmp', 'zip', 'wav']:\n            file_full_path = os.path.join(\"carved_files\", name)\n            thumbnail_folder = os.path.join(\"carved_files\", \"thumbnails\")  # Folder to save thumbnails\n\n            if not os.path.exists(thumbnail_folder):\n                os.makedirs(thumbnail_folder)  # Create the thumbnail folder if it doesn't exist\n\n            if type_.lower() == 'mov':\n                thumbnail_path = os.path.join(thumbnail_folder, name.replace('.mov', '.png'))\n                with VideoFileClip(file_full_path) as clip:\n                    clip.save_frame(thumbnail_path, t=0.5)  # save frame at 0.5 seconds\n                pixmap = QPixmap(thumbnail_path)\n\n            elif type_.lower() == 'pdf':\n                # Convert the first page of the PDF to a thumbnail\n                images = convert_from_path(file_full_path)\n                thumbnail_path = os.path.join(thumbnail_folder, name.replace('.pdf', '.png'))\n                images[0].save(thumbnail_path, 'PNG')\n                # Create the QPixmap from the full path\n                pixmap = QPixmap(thumbnail_path)\n\n            elif type_.lower() == 'wmv':\n                capture = cv2.VideoCapture(file_full_path)\n                success, image = capture.read()\n                capture.release()  # Release the capture object explicitly\n                if success:\n                    thumbnail_path = os.path.join(thumbnail_folder, name.replace('.wmv', '.png'))\n                    cv2.imwrite(thumbnail_path, image)\n                    pixmap = QPixmap(thumbnail_path)\n                else:\n                    print(\"Failed to extract thumbnail from WMV file\")\n\n            elif type_.lower() == 'zip':\n                # Render ZIP icon at target size for crisp display\n                pixmap = self.render_svg_to_pixmap('Icons/mimetypes/application-zip.svg', 120)\n\n            elif type_.lower() == 'wav':\n                # Render audio icon at target size for crisp display\n                pixmap = self.render_svg_to_pixmap('Icons/mimetypes/audio-x-generic.svg', 120)\n\n            else:\n                # For image files, use the original file path\n                thumbnail_path = file_full_path\n                pixmap = QPixmap(thumbnail_path)\n\n            # Center-crop to perfect square for modern uniform gallery look (skip for SVG icons)\n            if type_.lower() not in ['zip', 'wav']:\n                pixmap = self.center_crop_to_square(pixmap, 120)\n            icon = QIcon(pixmap)\n\n            # Create a QListWidgetItem, set its icon, and provide a size hint to ensure the text is visible\n            item = QListWidgetItem(icon, name)\n            # Set a compact size for the QListWidgetItem with minimal padding for text\n            item.setSizeHint(QSize(130, 145))\n\n            # Set the item flags to not be movable and to be selectable\n            item.setFlags(item.flags() & ~Qt.ItemIsDragEnabled & ~Qt.ItemIsDropEnabled)\n\n            # Add the QListWidgetItem to the list widget\n            self.list_widget.addItem(item)\n\n    def clear(self):\n        self.table_widget.setRowCount(0)\n        self.list_widget.clear()\n        self.carved_files.clear()\n        self.start_button.setEnabled(True)\n        self.stop_button.setEnabled(False)\n\n    def clear_ui(self):\n        self.table_widget.setRowCount(0)\n        self.list_widget.clear()\n\n    def handle_resize_event(self, event):\n        # Calculate total width of the table\n        total_width = self.table_widget.width()\n\n        # Fixed columns: Id, Size, Type, Modification Date\n        fixed_width = (self.table_widget.columnWidth(0) +  # Id\n                       self.table_widget.columnWidth(2) +  # Size\n                       self.table_widget.columnWidth(3) +  # Type\n                       self.table_widget.columnWidth(4))  # Modification Date\n\n        # Remaining space for dynamic columns\n        remaining_width = total_width - fixed_width\n\n        # Allocate remaining space proportionally\n        self.table_widget.setColumnWidth(1, remaining_width // 2)  # Name column\n        self.table_widget.setColumnWidth(5, remaining_width // 2)  # File Path column\n\n        super(QTableWidget, self.table_widget).resizeEvent(event)\n"
  },
  {
    "path": "modules/hex_tab.py",
    "content": "import os\nfrom functools import lru_cache\n\nfrom PySide6.QtCore import Qt, QObject, Signal, QThread, QSize\nfrom PySide6.QtGui import QAction, QIcon, QFont, QResizeEvent\nfrom PySide6.QtWidgets import (QToolBar, QLabel, QMessageBox, QWidget, QVBoxLayout,\n                               QLineEdit, QTableWidget, QHeaderView, QTableWidgetItem, QListWidget,\n                               QSizePolicy, QFrame, QApplication, QMenu, QAbstractItemView, QFileDialog,\n                               QToolButton, QComboBox, QSplitter)\n\n\nclass SearchWorker(QObject):\n    search_finished = Signal(list)\n\n    def __init__(self, hex_viewer_manager, query):\n        super().__init__()\n        self.hex_viewer_manager = hex_viewer_manager\n        self.query = query\n\n    def run(self):\n        matches = self.hex_viewer_manager.search(self.query)\n        self.search_finished.emit(matches)\n\n\nclass HexViewerManager:\n    LINES_PER_PAGE = 1024\n\n    def __init__(self, hex_content, byte_content):\n        self.hex_content = hex_content\n        self.byte_content = byte_content\n        self.num_total_pages = (len(hex_content) // 32) // self.LINES_PER_PAGE\n        if (len(hex_content) // 32) % self.LINES_PER_PAGE:\n            self.num_total_pages += 1\n\n    @lru_cache(maxsize=None)\n    def format_hex(self, page=0):\n        start_index = page * self.LINES_PER_PAGE * 32\n        end_index = start_index + (self.LINES_PER_PAGE * 32)\n        lines = []\n        chunk_starts = range(start_index, end_index, 32)\n        for start in chunk_starts:\n            if start >= len(self.hex_content):\n                break\n            lines.append(self.format_hex_chunk(start))\n        return '\\n'.join(lines)\n\n    def format_hex_chunk(self, start):\n        hex_part = []\n        ascii_repr = []\n        for j in range(start, start + 32, 2):\n            chunk = self.hex_content[j:j + 2]\n            if not chunk:\n                break\n            chunk_int = int(chunk, 16)\n            hex_part.append(chunk.upper())\n            ascii_repr.append(chr(chunk_int) if 32 <= chunk_int <= 126 else '.')\n        hex_line = ' '.join(hex_part)\n        padding = ' ' * (48 - len(hex_line))\n        ascii_line = ''.join(ascii_repr)\n        line = f'0x{start // 2:08x}: {hex_line}{padding}  {ascii_line}'\n        return line\n\n    def total_pages(self):\n        return self.num_total_pages\n\n    def search(self, query):\n        if all(part.isalnum() or part.isspace() for part in query.split()):\n            try:\n                query_bytes = bytes.fromhex(query.replace(\" \", \"\"))\n                return self.search_by_hex(query_bytes)\n            except ValueError:\n                pass  # Invalid hex value\n\n        if query.startswith(\"0x\"):\n            return self.search_by_address(query)\n        else:\n            return self.search_by_string(query)\n\n    def search_by_address(self, address):\n        \"\"\"Searches for the line that contains the given address (offset)\"\"\"\n        try:\n            address_int = int(address, 16)\n            line_number = address_int // 16\n            if 0 <= line_number < len(self.byte_content) // 16:\n                return [line_number]\n            else:\n                return []\n        except ValueError:\n            return []\n\n    def search_by_string(self, query):\n        # Implementation for searching by string\n        matches = []\n        query_bytes = query.encode('utf-8')\n\n        start = 0\n        while start < len(self.byte_content):\n            position = self.byte_content.find(query_bytes, start)\n            if position == -1:\n                break\n            start = position + 1  # Move the start to the next character\n            line_number = position // 16  # Calculate line number\n            matches.append(line_number)\n\n        return matches\n\n    def search_by_hex(self, hex_query):\n        if all(part.isalnum() for part in hex_query.split()):\n            try:\n                query_bytes = bytes.fromhex(hex_query.replace(\" \", \"\"))\n            except ValueError:\n                return []  # Invalid hex value\n        else:\n            return []  # Non-alphanumeric characters in the query\n\n        matches = []\n        start = 0\n        while start < len(self.byte_content):\n            position = self.byte_content[start:].find(query_bytes)\n            if position == -1:\n                break\n            start += position  # Adjust the start to the found position\n            line_number = start // 16  # Calculate line number\n            matches.append(line_number)\n            start += len(query_bytes)  # Move past the current match\n        return matches\n\n\nclass HexViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.hex_viewer_manager = None\n        self.current_page = 0\n\n        self.context_menu = QMenu(self)\n        self.copy_action = QAction(\"Copy\", self)\n        self.copy_action.triggered.connect(self.copy_to_clipboard)\n        self.context_menu.addAction(self.copy_action)\n\n        # Set up a context menu event handler\n        self.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.customContextMenuRequested.connect(self.show_context_menu)\n\n        self.initialize_ui()\n\n    def show_context_menu(self, pos):\n        # Show the context menu at the cursor position\n        self.context_menu.exec_(self.mapToGlobal(pos))\n\n    def copy_to_clipboard(self):\n        selected_text = \"\"\n\n        # Check if any cells in the hex_table are selected\n        selected_indexes = self.hex_table.selectedIndexes()\n        if selected_indexes:\n            # Sort the selected indexes by row\n            selected_indexes.sort(key=lambda index: index.row())\n\n            for i, index in enumerate(selected_indexes):\n                selected_text += index.data(Qt.DisplayRole)\n\n                if index.column() == 16:  # The last column (ASCII), add a newline\n                    selected_text += \"\\n\"\n                else:\n                    next_index = selected_indexes[i + 1] if i + 1 < len(selected_indexes) else None\n\n                    # Add a space if the next cell is in the same row\n                    if next_index and next_index.row() == index.row():\n                        selected_text += \" \"\n\n        # Copy the selected text to the clipboard\n        if selected_text:\n            clipboard = QApplication.clipboard()\n            clipboard.setText(selected_text)\n\n    def initialize_ui(self):\n        self.layout = QVBoxLayout()\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n        self.layout.setAlignment(Qt.AlignCenter)\n\n        self.setup_toolbar()\n        self.layout.addWidget(self.toolbar)\n\n        # Create a QSplitter for dynamic resizing\n        self.splitter = QSplitter(Qt.Horizontal, self)  # Horizontal splitter for hex_table and search_results_frame\n\n        # Setup Hex Table\n        self.setup_hex_table()\n        self.hex_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)  # Make hex table expandable\n\n        # Add the hex table to the splitter\n        self.splitter.addWidget(self.hex_table)\n\n        # Create a QVBoxLayout for the search results and its title\n        self.search_results_layout = QVBoxLayout()\n        self.search_results_layout.setContentsMargins(2, 2, 2, 2)\n        self.search_results_layout.setSpacing(2)\n\n        self.search_results_frame = QFrame(self)  # This frame will contain the title and the search results\n        self.search_results_frame.setMaximumWidth(180)  # Make it narrower\n        self.search_results_frame.setObjectName(\"search_results_frame\")  # Set object name for stylesheet targeting\n        self.search_results_frame.setSizePolicy(QSizePolicy.Fixed,\n                                                QSizePolicy.Expanding)  # Fixed width, expandable height\n\n        self.search_results_title = QLabel(\"Search Results\", self.search_results_frame)\n        self.search_results_title.setAlignment(Qt.AlignCenter)\n        self.search_results_title.setFixedHeight(22)\n        self.search_results_title.setObjectName(\"search_results_title\")  # Set object name for stylesheet targeting\n        self.search_results_layout.addWidget(self.search_results_title)\n\n        self.search_results_widget = QListWidget(self.search_results_frame)\n        self.search_results_widget.setObjectName(\"search_results_widget\")  # Set object name for stylesheet targeting\n        self.search_results_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)  # Show scroll bar when needed\n        self.search_results_widget.itemClicked.connect(self.search_result_clicked)\n        self.search_results_widget.setFont(QFont(\"Courier\", 9))  # Smaller font\n        self.search_results_layout.addWidget(self.search_results_widget)\n\n        self.search_results_frame.setLayout(self.search_results_layout)\n\n        # Add the search results frame to the splitter\n        self.splitter.addWidget(self.search_results_frame)\n\n        # Set both widgets to expand in both directions\n        self.splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n        # Add the splitter to the main layout\n        self.layout.addWidget(self.splitter)\n\n        # Set the main layout\n        self.setLayout(self.layout)\n\n    def resizeEvent(self, event: QResizeEvent):\n        \"\"\"Handle window resizing to update layout.\"\"\"\n        # Adjust splitter sizes dynamically based on new window dimensions\n        total_width = event.size().width()\n        total_height = event.size().height()\n\n        # Set sizes for horizontal splitter: 75% for hex_table and 25% for search_results_frame\n        self.splitter.setSizes([int(total_width * 0.75), int(total_width * 0.25)])\n\n        super().resizeEvent(event)\n\n    def setup_toolbar(self):\n        self.toolbar = QToolBar(self)\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.toolbar.setMovable(False)\n        self.toolbar.setIconSize(QSize(16, 16))  # Reduce icon size\n        self.toolbar.setFixedHeight(32)  # Reduce toolbar height\n        self.toolbar.setStyleSheet(\"\"\"\n            QToolBar {\n                spacing: 2px;\n                padding: 1px;\n            }\n            QToolButton {\n                padding: 2px;\n                margin: 1px;\n            }\n        \"\"\")\n        # disable right click\n        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)\n\n        # Navigation buttons\n        self.first_action = QAction(QIcon(\"Icons/icons8-thick-arrow-pointing-up-50.png\"), \"First\", self)\n        self.first_action.triggered.connect(self.load_first_page)\n        self.toolbar.addAction(self.first_action)\n\n        self.prev_action = QAction(QIcon(\"Icons/icons8-left-arrow-50.png\"), \"Previous\", self)\n        self.prev_action.triggered.connect(self.previous_page)\n        self.toolbar.addAction(self.prev_action)\n\n        # Page entry\n        self.page_entry = QLineEdit(self)\n        self.page_entry.setMaximumWidth(40)\n        self.page_entry.setFixedHeight(25)  # Set fixed height for input\n        self.page_entry.setPlaceholderText(\"1\")\n        self.page_entry.returnPressed.connect(self.go_to_page_by_entry)\n        self.toolbar.addWidget(self.page_entry)\n\n        # Total pages label\n        self.total_pages_label = QLabel(\" of \")\n        self.total_pages_label.setFixedHeight(25)  # Set fixed height for label\n        self.toolbar.addWidget(self.total_pages_label)\n\n        self.next_action = QAction(QIcon(\"Icons/icons8-right-arrow-50.png\"), \"Next\", self)\n        self.next_action.triggered.connect(self.next_page)\n        self.toolbar.addAction(self.next_action)\n\n        self.last_action = QAction(QIcon(\"Icons/icons8-down-50.png\"), \"Last\", self)\n        self.last_action.triggered.connect(self.load_last_page)\n        self.toolbar.addAction(self.last_action)\n\n        # Add a small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        # Add a QLabel and a QComboBox for font size to the toolbar\n        font_label = QLabel(\"Font Size: \")\n        font_label.setFixedHeight(25)  # Set fixed height for label\n        self.toolbar.addWidget(font_label)\n\n        self.font_size_combobox = QComboBox(self)\n        self.font_size_combobox.setFixedHeight(25)  # Set fixed height for combobox\n        self.font_size_combobox.setFixedWidth(60)  # Increase width to show full numbers\n        self.font_size_combobox.addItems([\"8\", \"10\", \"12\", \"14\", \"16\", \"18\", \"20\", \"24\", \"28\", \"32\", \"36\"])\n        self.font_size_combobox.currentTextChanged.connect(self.update_font_size)\n        self.toolbar.addWidget(self.font_size_combobox)\n\n        # Add small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        self.export_button = QToolButton(self)\n        self.export_button.setObjectName(\"exportButton\")  # Assign a unique object name\n        self.export_button.setText(\"Export\")\n        self.export_button.setToolButtonStyle(Qt.ToolButtonTextOnly)  # Change to text only since no icon is used\n        self.export_button.setFixedHeight(25)  # Set fixed height\n        self.export_button.setFixedWidth(100)  # Set fixed width to show full text\n        self.export_button.setPopupMode(QToolButton.MenuButtonPopup)  # Set the popup mode\n\n        # Add format options to the menu\n        self.export_menu = QMenu(self)\n\n        self.text_format_action = QAction(\"Text (.txt)\", self)\n        self.text_format_action.triggered.connect(lambda: self.export_content(\"txt\"))\n        self.export_menu.addAction(self.text_format_action)\n\n        self.html_format_action = QAction(\"HTML (.html)\", self)\n        self.html_format_action.triggered.connect(lambda: self.export_content(\"html\"))\n        self.export_menu.addAction(self.html_format_action)\n\n        self.export_button.setMenu(self.export_menu)\n        self.toolbar.addWidget(self.export_button)\n\n        # Add a spacer to push the following widgets to the right\n        spacer = QWidget(self)\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(spacer)\n\n        # Search bar components\n        self.search_bar = QLineEdit(self)\n        self.search_bar.setMaximumWidth(180)  # Reduce width to save space\n        self.search_bar.setFixedHeight(25)  # Reduce height\n        self.search_bar.setContentsMargins(5, 0, 5, 0)  # Reduce margins\n        self.search_bar.setPlaceholderText(\"Search...\")\n        self.search_bar.returnPressed.connect(self.trigger_search)\n        self.toolbar.addWidget(self.search_bar)\n\n    def update_font_size(self):\n        # Get the current font size from the combobox\n        selected_size = int(self.font_size_combobox.currentText())\n\n        # Set the new font size to the hex_table\n        current_font = self.hex_table.font()\n        current_font.setPointSize(selected_size)\n        self.hex_table.setFont(current_font)\n\n        # Dynamically adjust column widths based on the font size\n        address_width = selected_size * 10  # Proportional width for Address column\n        byte_width = selected_size * 3  # Proportional width for each byte column\n        ascii_width = selected_size * 8  # Proportional width for ASCII column\n\n        # Set column widths dynamically\n        self.hex_table.setColumnWidth(0, address_width)  # Address column\n        for i in range(1, 17):  # Set uniform width for each byte column\n            self.hex_table.setColumnWidth(i, byte_width)\n        self.hex_table.setColumnWidth(17, ascii_width)  # ASCII column\n\n        # Update the font size for the headers as well\n        header_font = self.hex_table.horizontalHeader().font()\n        header_font.setPointSize(selected_size)\n        self.hex_table.horizontalHeader().setFont(header_font)\n\n        # Apply the font to all existing data cells\n        for row in range(self.hex_table.rowCount()):\n            for col in range(self.hex_table.columnCount()):\n                item = self.hex_table.item(row, col)\n                if item:\n                    item_font = item.font()\n                    # Ensure we have a valid font size before setting it\n                    if selected_size > 0:\n                        item_font.setPointSize(selected_size)\n                        item.setFont(item_font)\n\n        # Adjust the horizontal scrollbar policy if needed\n        if self.hex_table.horizontalHeader().length() > self.hex_table.viewport().width():\n            self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n        else:\n            self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n\n    def setup_hex_table(self):\n        self.hex_table = QTableWidget()\n        self.hex_table.verticalHeader().setDefaultSectionSize(20)  # Smaller row height\n\n        # Set the font of the hex_table\n        font = QFont(\"Courier\")\n        font.setPointSize(10)  # Default smaller font size\n        font.setLetterSpacing(QFont.AbsoluteSpacing, 1)  # Reduce letter spacing\n        self.hex_table.setFont(font)\n\n        # Configure the columns and headers\n        self.hex_table.setColumnCount(18)  # 16 bytes + 1 address + 1 ASCII\n        self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])\n        self.hex_table.verticalHeader().setVisible(False)\n\n        # Set resizing policies for the header\n        header = self.hex_table.horizontalHeader()\n        header.setStyleSheet(\"QHeaderView::section { padding: 2px; }\")  # Reduce header padding\n        header.setDefaultSectionSize(25)  # Set a smaller default size\n\n        # Address column - Resize based on content\n        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)\n\n        # Byte columns - Fixed width for each byte column\n        for i in range(1, 17):  # 00 to 0F columns\n            header.setSectionResizeMode(i, QHeaderView.Fixed)\n            self.hex_table.setColumnWidth(i, 25)  # Smaller byte column width\n\n        # ASCII column - Stretch to fill remaining space\n        header.setSectionResizeMode(17, QHeaderView.Stretch)\n\n        # Adjust for remaining space in the ASCII column\n        header.setStretchLastSection(True)\n\n        # Set the initial column widths\n        self.hex_table.setColumnWidth(0, 120)  # Address column initial width\n        self.hex_table.setColumnWidth(17, 200)  # ASCII column initial width\n\n        self.hex_table.setStyleSheet(\"\"\"\n            QTableWidget {\n                gridline-color: transparent;\n                border: 1px solid #d3d3d3;\n            }\n            QTableWidget::item {\n                padding: 0px;\n                border: none;\n            }\n        \"\"\")\n        self.hex_table.setShowGrid(False)\n        self.hex_table.setAlternatingRowColors(True)\n        self.hex_table.setEditTriggers(QAbstractItemView.NoEditTriggers)\n\n    def display_hex_content(self, file_content):\n        hex_content = file_content.hex()\n        self.search_results_widget.clear()\n        # self.search_results_frame.setVisible(False)\n\n        # Clear the search bar text\n        self.search_bar.setText(\"\")\n        self.hex_viewer_manager = HexViewerManager(hex_content, file_content)\n        self.update_navigation_states()\n        self.display_current_page()\n        # clear the page number entry\n        self.page_entry.setText(\"\")\n\n    def export_content(self, selected_format):\n        if not self.hex_viewer_manager:\n            QMessageBox.warning(self, \"No Content\", \"No content available to export.\")\n            return\n\n        options = QFileDialog.Options()\n        options |= QFileDialog.ReadOnly  # Allow read-only access to the selected file\n\n        if selected_format == \"txt\":\n            file_name, _ = QFileDialog.getSaveFileName(\n                self, \"Export Hex Content\", \"\", \"Text Files (*.txt)\", options=options\n            )\n            self.export_as_text(file_name)\n        elif selected_format == \"html\":\n            file_name, _ = QFileDialog.getSaveFileName(\n                self, \"Export Hex Content\", \"\", \"HTML Files (*.html)\", options=options\n            )\n            self.export_as_html(file_name)\n        else:\n            QMessageBox.warning(self, \"Unsupported Format\", \"Unsupported export format selected.\")\n\n    def export_as_text(self, file_name):\n        with open(file_name, \"w\") as text_file:\n            # Add the header line with green color using ANSI escape codes\n            header_line = \"Address     00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F        ASCII\"\n            text_file.write(header_line + \"\\n\")\n\n            # Add an empty line\n            text_file.write(\"\\n\")\n\n            # Write the formatted hex content\n            formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)\n            text_file.write(formatted_hex)\n\n    def export_as_html(self, file_name):\n        html_content = \"<html><body>\\n\"\n        html_content += \"<pre>\\n\"\n\n        # Add a smaller and less prominent header with the original text\n        header_line = '<div style=\"font-size:14px; color:#888;\">Generated by Trace</div>'\n        html_content += header_line + \"<br><br>\\n\"\n\n        # Add directory and file name information\n        directory, filename = os.path.split(file_name)\n        html_content += f'<span style=\"color:blue;\">Directory: {directory}</span><br>\\n'\n        html_content += f'<span style=\"color:blue;\">File Name: {filename}</span><br><br>\\n'\n\n        # Add the green header line\n        header_line = ('<span style=\"color:green;\">Address     00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F        '\n                       'ASCII</span>')\n        html_content += header_line + \"<br>\\n\"\n\n        html_content += self.hex_viewer_manager.format_hex(self.current_page).replace(\"\\n\", \"<br>\")\n        html_content += \"</pre>\\n\"\n        html_content += \"</body></html>\"\n\n        with open(file_name, \"w\") as html_file:\n            html_file.write(html_content)\n\n    def parse_hex_line(self, line):\n        if \":\" not in line:\n            return None, None, None\n        address, rest = line.split(\":\", maxsplit=1)\n        hex_chunk, ascii_repr = rest.split(\"  \", maxsplit=1)\n        return address.strip(), hex_chunk.strip(), ascii_repr.strip()\n\n    def clear_content(self):\n        self.hex_table.clear()\n\n    def load_first_page(self):\n        try:\n            self.current_page = 0\n            self.display_current_page()\n        except (AttributeError, IndexError) as e:\n            print(f\"Error occurred: {e}\")\n\n    def load_last_page(self):\n        try:\n            self.current_page = self.hex_viewer_manager.total_pages() - 1\n            self.display_current_page()\n        except (AttributeError, IndexError) as e:\n            print(f\"Error occurred: {e}\")\n\n    def next_page(self):\n        try:\n            if self.current_page < self.hex_viewer_manager.total_pages() - 1:\n                self.current_page += 1\n            self.display_current_page()\n        except (AttributeError, IndexError) as e:\n            print(f\"Error occurred: {e}\")\n\n    def previous_page(self):\n        try:\n            if self.current_page > 0:\n                self.current_page -= 1\n            self.display_current_page()\n        except (AttributeError, IndexError) as e:\n            print(f\"Error occurred: {e}\")\n\n    def search_result_clicked(self, item):\n        address = item.text().split(\":\")[1].strip()\n        self.navigate_to_address(address)\n\n    def display_current_page(self):\n        formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)\n\n        # Clear the table first\n        self.hex_table.setRowCount(0)\n        self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])\n\n        hex_lines = formatted_hex.split('\\n')\n\n        # Ensure we set the correct row count\n        self.hex_table.setRowCount(len(hex_lines))\n\n        # Get the current font size with fallback to default\n        current_font = self.hex_table.font()\n        current_font_size = current_font.pointSize()\n        if current_font_size <= 0:  # Use default if invalid\n            current_font_size = 10  # Default font size\n\n        for row, line in enumerate(hex_lines):\n            address, hex_chunk, ascii_repr = self.parse_hex_line(line)\n            if not address or not hex_chunk:  # Skip if there's an error in parsing\n                continue\n\n            # Set address and center-align\n            address_item = QTableWidgetItem(address + \":\")  # Add a colon after the address\n            address_item.setTextAlignment(Qt.AlignCenter)\n            item_font = address_item.font()\n            item_font.setPointSize(current_font_size)\n            address_item.setFont(item_font)\n            self.hex_table.setItem(row, 0, address_item)\n\n            # Set hex values and center-align\n            for col, byte in enumerate(hex_chunk.split()):\n                byte_item = QTableWidgetItem(byte)\n                byte_item.setTextAlignment(Qt.AlignCenter)\n                byte_item.setBackground(Qt.white)  # Clear any previous highlight\n                item_font = byte_item.font()\n                item_font.setPointSize(current_font_size)\n                byte_item.setFont(item_font)\n                self.hex_table.setItem(row, col + 1, byte_item)\n\n            # Set ASCII representation and center-align\n            ascii_item = QTableWidgetItem(ascii_repr)\n            ascii_item.setTextAlignment(Qt.AlignCenter)\n            item_font = ascii_item.font()\n            item_font.setPointSize(current_font_size)\n            ascii_item.setFont(item_font)\n            self.hex_table.setItem(row, 17, ascii_item)\n\n        self.update_navigation_states()\n\n    def go_to_page_by_entry(self):\n        try:\n            page_num = int(self.page_entry.text()) - 1\n            if 0 <= page_num < self.hex_viewer_manager.total_pages():\n                self.current_page = page_num\n                self.display_current_page()\n                self.update_navigation_states()\n            else:\n                QMessageBox.warning(self, \"Invalid Page\", \"Page number out of range.\")\n        except ValueError:\n            QMessageBox.warning(self, \"Invalid Page\", \"Please enter a valid page number.\")\n\n    def update_navigation_states(self):\n        if not self.hex_viewer_manager:\n            self.prev_action.setEnabled(False)\n            self.next_action.setEnabled(False)\n            return\n\n        self.prev_action.setEnabled(self.current_page > 0)\n        self.next_action.setEnabled(self.current_page < self.hex_viewer_manager.total_pages() - 1)\n        self.page_entry.setText(str(self.current_page + 1))\n        self.total_pages_label.setText(f\"of {self.hex_viewer_manager.total_pages()}\")\n\n    def update_total_pages_label(self):\n        total_pages = self.hex_viewer_manager.total_pages()\n        current_page = self.current_page + 1\n        self.total_pages_label.setText(f\"{current_page} of {total_pages}\")\n\n    def trigger_search(self):\n        query = self.search_bar.text()\n        if not query:\n            QMessageBox.warning(self, \"Search Error\", \"Please enter a search query.\")\n            return\n\n        # Check if a search is already ongoing. If so, stop it before starting a new one.\n        if hasattr(self, 'search_thread') and self.search_thread.isRunning():\n            self.search_thread.quit()\n            self.search_thread.wait()\n\n        # Start the search in a new thread\n        self.search_thread = QThread()\n        self.search_worker = SearchWorker(self.hex_viewer_manager, query)\n        self.search_worker.moveToThread(self.search_thread)\n\n        # Connect signals and slots\n        self.search_worker.search_finished.connect(self.handle_search_results)\n        self.search_thread.started.connect(self.search_worker.run)\n        self.search_thread.finished.connect(self.cleanup_thread_resources)\n\n        # Start the thread\n        self.search_thread.start()\n\n    def cleanup_thread_resources(self):\n        # Ensure safe cleanup by checking the existence of resources before deleting\n        if hasattr(self, 'search_worker'):\n            self.search_worker.deleteLater()\n            del self.search_worker\n        if hasattr(self, 'search_thread'):\n            self.search_thread.deleteLater()\n            del self.search_thread\n\n    def closeEvent(self, event):\n        if hasattr(self, 'search_thread') and self.search_thread.isRunning():\n            self.search_thread.quit()\n            self.search_thread.wait()\n        super().closeEvent(event)\n\n    def handle_search_results(self, matches):\n        self.search_results_widget.clear()  # Clear previous results\n        if matches:\n            for match in matches:\n                address = f\"0x{match * 16:08x}\"  # Calculate the address from line number\n                self.search_results_widget.addItem(f\"Address: {address}\")\n\n            # Show the search results frame and resize the splitter to allocate more space to results\n            self.search_results_frame.setVisible(True)\n            self.splitter.setSizes([self.width() * 0.6, self.width() * 0.4])  # Adjust sizes dynamically\n\n        else:\n            QMessageBox.warning(self, \"Search Result\", \"No matches found.\")\n            # Even if no matches are found, the search results frame will still be shown\n            self.splitter.setSizes([self.width() * 0.75, self.width() * 0.25])\n\n    def navigate_to_address(self, address):\n        try:\n            # Convert the address string back to an integer\n            address_int = int(address, 16)\n\n            # Determine the line number from the address\n            line = address_int // 16\n\n            # The rest of the logic remains the same\n            self.current_page = line // self.hex_viewer_manager.LINES_PER_PAGE\n            self.display_current_page()\n\n            # Navigate to the specific row on that page and highlight it\n            row_in_page = line % self.hex_viewer_manager.LINES_PER_PAGE\n            self.hex_table.selectRow(row_in_page)\n            for col in range(1, 17):\n                item = self.hex_table.item(row_in_page, col)\n                if item:\n                    item.setBackground(Qt.yellow)\n            self.update_navigation_states()\n        except ValueError:\n            QMessageBox.warning(self, \"Navigation Error\", \"Invalid address.\")\n"
  },
  {
    "path": "modules/mainwindow.py",
    "content": "import configparser\nimport hashlib\nimport os\nimport datetime\nimport pyewf\nimport pytsk3\nimport tempfile\nimport gc\nimport time\nimport logging\nimport re\nfrom typing import Optional, Dict, Any, List, Tuple\nfrom Registry import Registry\nfrom sqlite3 import connect as sqlite3_connect\nimport subprocess\nimport platform\nfrom contextlib import contextmanager\nfrom functools import lru_cache\nfrom PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QMargins\nfrom PySide6.QtGui import QIcon, QFont, QPalette, QBrush, QAction, QActionGroup, QPixmap, QPainter, QColor\nfrom PySide6.QtCharts import QChart, QChartView, QPieSeries, QPieSlice\nfrom PySide6.QtWidgets import (QMainWindow, QMenuBar, QMenu, QToolBar, QDockWidget, QTreeWidget, QTabWidget,\n                               QFileDialog, QTreeWidgetItem, QTableWidget, QMessageBox, QTableWidgetItem,\n                               QDialog, QVBoxLayout, QHBoxLayout, QInputDialog, QDialogButtonBox, QHeaderView, QLabel, QLineEdit,\n                               QFormLayout, QApplication, QWidget, QProgressDialog, QSizePolicy, QGroupBox,\n                               QCheckBox, QGridLayout, QScrollArea, QPushButton, QToolButton)\n\nfrom modules.about import AboutDialog\nfrom modules.converter import Main\nfrom modules.exif_tab import ExifViewer\nfrom modules.file_carving import FileCarvingWidget\nfrom modules.hex_tab import HexViewer\nfrom modules.metadata_tab import MetadataViewer\nfrom modules.registry import RegistryExtractor\nfrom modules.text_tab import TextViewer\nfrom modules.unified_application_manager import UnifiedViewer\nfrom modules.verification import VerificationWidget\nfrom modules.veriphone_api import VeriphoneWidget\nfrom modules.virus_total_tab import VirusTotal\n\nSECTOR_SIZE = 512\nCHUNK_SIZE = 4 * 1024 * 1024  # 4MB chunks for processing\nFILE_BUFFER_SIZE = 4096  # 4KB for file operations\n\n# ==================== CONFIGURATION CONSTANTS ====================\n# Logger setup\nlogger = logging.getLogger('TRACE.MainWindow')\n\n# Window dimensions\nDEFAULT_WINDOW_WIDTH = 1200\nDEFAULT_WINDOW_HEIGHT = 800\nDEFAULT_WINDOW_X = 100\nDEFAULT_WINDOW_Y = 100\n\n# Dock sizes\nVIEWER_DOCK_MIN_HEIGHT = 222\nVIEWER_DOCK_MAX_WIDTH = 1200\nVIEWER_DOCK_MAX_SIZE = 16777215  # Qt maximum size value\n\n# Column widths for listing table\nCOLUMN_WIDTHS = {\n    'name': 400,        # Widest - file names can be long\n    'inode': 50,        # Compact - numbers don't vary much\n    'type': 50,         # Compact - short text like \"File\", \"Dir\"\n    'size': 100,         # Compact - formatted sizes\n    'created': 160,      # Narrower - timestamps are consistent length\n    'accessed': 160,     # Narrower - timestamps are consistent length\n    'modified': 160,     # Narrower - timestamps are consistent length\n    'changed': 160,      # Narrower - timestamps are consistent length\n    'path': 1100         # Wide - paths can be long\n}\n\n# Progress dialog settings\nPROGRESS_DIALOG_WIDTH = 300\n\n# Timeouts (in seconds)\nMOUNT_TIMEOUT = 30\nINFO_TIMEOUT = 10\nPROCESS_TIMEOUT = 30\nTHREAD_SLEEP_MS = 1000  # milliseconds\n\n# Minimum duration for progress dialog (milliseconds)\nPROGRESS_MIN_DURATION = 1500\n\n# Icon size\nTREE_ICON_SIZE = 16\nTABLE_ICON_SIZE = 24\nTOOLBAR_ICON_SIZE = 16\n\n# Table settings\nTABLE_COLUMN_COUNT = 9\nTABLE_BATCH_SIZE = 200  # Number of rows to process before updating UI\n\n# Input field settings\nINPUT_FIELD_MIN_WIDTH = 400\nAPI_DIALOG_WIDTH = 600\n\n# Qt maximum size constant\nQT_MAX_SIZE = 16777215\n# ================================================================\n\n\n# Define a utility function for safe datetime conversion\ndef safe_datetime(timestamp):\n    if timestamp is None or timestamp == 0:\n        return \"N/A\"\n    try:\n        return datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + \" UTC\"\n    except Exception:\n        return \"N/A\"\n\n\n# Utility class for common operations\nclass FileSystemUtils:\n    @staticmethod\n    def get_readable_size(size_in_bytes):\n        \"\"\"Convert bytes to a human-readable string (e.g., KB, MB, GB, TB).\"\"\"\n        if size_in_bytes is None:\n            return \"0 B\"\n\n        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:\n            if size_in_bytes < 1024.0:\n                return f\"{size_in_bytes:.2f} {unit}\"\n            size_in_bytes /= 1024.0\n        return f\"{size_in_bytes:.2f} PB\"\n\n    @staticmethod\n    @contextmanager\n    def temp_file():\n        \"\"\"Context manager for temporary files, ensuring cleanup.\"\"\"\n        temp_path = None\n        try:\n            with tempfile.NamedTemporaryFile(delete=False) as temp:\n                temp_path = temp.name\n                yield temp_path\n        finally:\n            if temp_path and os.path.exists(temp_path):\n                os.remove(temp_path)\n\n\n# Class to handle EWF images\nclass EWFImgInfo(pytsk3.Img_Info):\n    def __init__(self, ewf_handle):\n        self._ewf_handle = ewf_handle\n        super(EWFImgInfo, self).__init__(url=\"\", type=pytsk3.TSK_IMG_TYPE_EXTERNAL)\n\n    def close(self):\n        self._ewf_handle.close()\n\n    def read(self, offset, size):\n        self._ewf_handle.seek(offset)\n        return self._ewf_handle.read(size)\n\n    def get_size(self):\n        return self._ewf_handle.get_media_size()\n\n\n# ImageHandler class with optimizations\nclass ImageHandler:\n    def __init__(self, image_path):\n        self.image_path = image_path\n        self.img_info = None\n        self.volume_info = None\n        self.fs_info_cache = {}\n        self.fs_info = None\n        self.is_wiped_image = False\n        self._directory_cache = {}  # Cache for directory contents\n        self._partition_cache = None  # Cache for partitions\n\n        # Load the image with progress tracking\n        self.load_image()\n\n    def __del__(self):\n        \"\"\"Cleanup resources when the object is destroyed.\"\"\"\n        self.close_resources()\n\n    def close_resources(self):\n        \"\"\"Explicitly close all open resources.\"\"\"\n        # Close filesystem objects\n        for fs_info in self.fs_info_cache.values():\n            if hasattr(fs_info, 'close'):\n                try:\n                    fs_info.close()\n                except:\n                    pass\n\n        # Close the image\n        if self.img_info:\n            if hasattr(self.img_info, 'close'):\n                try:\n                    self.img_info.close()\n                except:\n                    pass\n            self.img_info = None\n\n        # Clear caches\n        self.fs_info_cache.clear()\n        self._directory_cache.clear()\n\n    def get_size(self):\n        \"\"\"Returns the size of the disk image.\"\"\"\n        if self.img_info:\n            return self.img_info.get_size()\n        else:\n            raise AttributeError(\"Image not loaded or unsupported format.\")\n\n    def read(self, offset, size):\n        \"\"\"Reads data from the image starting at `offset` for `size` bytes.\"\"\"\n        if self.img_info and hasattr(self.img_info, 'read'):\n            return self.img_info.read(offset, size)\n        else:\n            raise NotImplementedError(\"The image format does not support direct reading.\")\n\n    def build_allocation_map(self, start_offset):\n        \"\"\"Build a map of allocated disk regions by traversing the filesystem.\"\"\"\n        allocation_map = []\n\n        try:\n            fs_info = self.get_fs_info(start_offset)\n            if not fs_info:\n                logger.warning(f\"Unable to get filesystem info for offset {start_offset}\")\n                return allocation_map\n\n            # Get block size for this filesystem\n            block_size = fs_info.info.block_size\n\n            # Recursively walk filesystem to find all allocated files\n            def walk_directory(directory, path=\"/\"):\n                \"\"\"Recursively walk directory and collect allocated file ranges.\"\"\"\n                try:\n                    for entry in directory:\n                        # Skip current and parent directory entries\n                        if not hasattr(entry, 'info') or not hasattr(entry.info, 'name'):\n                            continue\n\n                        name = entry.info.name.name.decode('utf-8', errors='ignore')\n                        if name in [\".\", \"..\"]:\n                            continue\n\n                        # Check if this is an allocated file\n                        if not hasattr(entry.info, 'meta') or entry.info.meta is None:\n                            continue\n\n                        # Only process allocated files (skip deleted files)\n                        is_allocated = bool(int(entry.info.meta.flags) & pytsk3.TSK_FS_META_FLAG_ALLOC)\n                        if not is_allocated:\n                            continue\n\n                        # Get file size and inode\n                        file_size = entry.info.meta.size\n\n                        # Only process files with actual data\n                        if file_size > 0:\n                            try:\n                                # Open the file to access its data runs\n                                file_obj = fs_info.open_meta(inode=entry.info.meta.addr)\n\n                                # Calculate byte offsets for the file's data\n                                # This is approximate - we use the file's logical position\n                                # For a more accurate map, we'd need to walk data runs\n                                # but this is a reasonable approximation for most filesystems\n\n                                # Get partition offset in bytes\n                                partition_offset_bytes = start_offset * 512\n\n                                # For simplicity, we'll mark regions based on inode metadata\n                                # A more sophisticated approach would walk TSK_FS_BLOCK structures\n                                # but pytsk3 doesn't expose block_walk easily\n\n                                # Estimate file location based on inode number and size\n                                # This is a simplified approach - actual blocks may be fragmented\n                                inode_addr = entry.info.meta.addr\n                                estimated_start = partition_offset_bytes + (inode_addr * block_size)\n                                estimated_end = estimated_start + file_size\n\n                                allocation_map.append((estimated_start, estimated_end))\n\n                            except Exception as e:\n                                # Skip files we can't open\n                                logger.debug(f\"Could not process file {path}{name}: {e}\")\n                                pass\n\n                        # Recursively process directories\n                        if entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:\n                            try:\n                                sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)\n                                walk_directory(sub_directory, f\"{path}{name}/\")\n                            except Exception as e:\n                                logger.debug(f\"Could not open directory {path}{name}: {e}\")\n                                pass\n\n                except Exception as e:\n                    logger.debug(f\"Error walking directory {path}: {e}\")\n                    pass\n\n            # Start walking from root directory\n            try:\n                root_dir = fs_info.open_dir(path=\"/\")\n                walk_directory(root_dir)\n            except Exception as e:\n                logger.error(f\"Error accessing root directory: {e}\")\n\n            # Sort allocation map by start offset for efficient searching\n            allocation_map.sort(key=lambda x: x[0])\n\n            logger.info(f\"Built allocation map with {len(allocation_map)} allocated file regions\")\n\n        except Exception as e:\n            logger.error(f\"Error building allocation map: {e}\")\n\n        return allocation_map\n\n    def get_image_type(self):\n        \"\"\"Determine the type of the image based on its extension.\"\"\"\n        _, extension = os.path.splitext(self.image_path)\n        extension = extension.lower()\n\n        ewf = [\".e01\", \".s01\", \".l01\", \".ex01\"]\n        raw = [\".raw\", \".img\", \".dd\", \".iso\",\n               \".ad1\", \".001\", \".dmg\", \".sparse\",\n               \".sparseimage\"]\n\n        if extension in ewf:\n            return \"ewf\"\n        elif extension in raw:\n            return \"raw\"\n        else:\n            raise ValueError(f\"Unsupported image type: {extension}\")\n\n    def calculate_hashes(self, progress_callback=None):\n        \"\"\"Calculate the MD5, SHA1, and SHA256 hashes for the image with progress reporting.\"\"\"\n        hash_md5 = hashlib.md5()\n        hash_sha1 = hashlib.sha1()\n        hash_sha256 = hashlib.sha256()\n        size = 0\n        total_size = 0\n        stored_md5, stored_sha1 = None, None\n\n        image_type = self.get_image_type()\n\n        try:\n            # First get total size for progress reporting\n            if image_type == \"ewf\":\n                filenames = pyewf.glob(self.image_path)\n                ewf_handle = pyewf.handle()\n                try:\n                    ewf_handle.open(filenames)\n                    total_size = ewf_handle.get_media_size()\n\n                    try:\n                        # Attempt to retrieve the stored hash values\n                        stored_md5 = ewf_handle.get_hash_value(\"MD5\")\n                        stored_sha1 = ewf_handle.get_hash_value(\"SHA1\")\n                    except Exception as e:\n                        logger.warning(f\"Unable to retrieve stored hash values: {e}\")\n\n                    # Calculate hashes in chunks\n                    while True:\n                        chunk = ewf_handle.read(CHUNK_SIZE)\n                        if not chunk:\n                            break\n\n                        hash_md5.update(chunk)\n                        hash_sha1.update(chunk)\n                        hash_sha256.update(chunk)\n                        size += len(chunk)\n\n                        # Report progress safely\n                        if progress_callback and total_size > 0:\n                            try:\n                                progress_callback(size, total_size)\n                            except Exception as e:\n                                logger.error(f\"Progress callback error: {e}\")\n                finally:\n                    ewf_handle.close()\n\n            elif image_type == \"raw\":\n                try:\n                    total_size = os.path.getsize(self.image_path)\n                    with open(self.image_path, \"rb\") as f:\n                        while True:\n                            chunk = f.read(CHUNK_SIZE)\n                            if not chunk:\n                                break\n\n                            hash_md5.update(chunk)\n                            hash_sha1.update(chunk)\n                            hash_sha256.update(chunk)\n                            size += len(chunk)\n\n                            # Report progress safely\n                            if progress_callback and total_size > 0:\n                                try:\n                                    progress_callback(size, total_size)\n                                except Exception as e:\n                                    logger.error(f\"Progress callback error: {e}\")\n                except Exception as e:\n                    logger.error(f\"Error reading raw image: {e}\")\n\n            # Compile the computed and stored hashes in a dictionary\n            hashes = {\n                'computed_md5': hash_md5.hexdigest(),\n                'computed_sha1': hash_sha1.hexdigest(),\n                'computed_sha256': hash_sha256.hexdigest(),\n                'size': size,\n                'path': self.image_path,\n                'stored_md5': stored_md5,\n                'stored_sha1': stored_sha1\n            }\n\n            return hashes\n        except Exception as e:\n            logger.error(f\"Error calculating hashes: {e}\")\n            return {\n                'computed_md5': 'Error',\n                'computed_sha1': 'Error',\n                'computed_sha256': 'Error',\n                'size': 0,\n                'path': self.image_path,\n                'stored_md5': None,\n                'stored_sha1': None,\n                'error': str(e)\n            }\n\n    def load_image(self):\n        \"\"\"Load the image and retrieve volume and filesystem information.\"\"\"\n        image_type = self.get_image_type()\n\n        try:\n            if image_type == \"ewf\":\n                filenames = pyewf.glob(self.image_path)\n                ewf_handle = pyewf.handle()\n                ewf_handle.open(filenames)\n                self.img_info = EWFImgInfo(ewf_handle)\n            elif image_type == \"raw\":\n                self.img_info = pytsk3.Img_Info(self.image_path)\n            else:\n                raise ValueError(f\"Unsupported image type: {image_type}\")\n\n            try:\n                self.volume_info = pytsk3.Volume_Info(self.img_info)\n            except Exception:\n                self.volume_info = None\n                # Attempt to detect a filesystem directly if no volume info\n                try:\n                    self.fs_info = pytsk3.FS_Info(self.img_info)\n                except Exception:\n                    self.fs_info = None\n                    # If no volume info and no filesystem, mark as wiped\n                    self.is_wiped_image = True\n        except Exception as e:\n            logger.error(f\"Error loading image: {e}\")\n            self.img_info = None\n            self.volume_info = None\n            self.fs_info = None\n            self.is_wiped_image = True\n\n    def has_filesystem(self, start_offset):\n        fs_info = self.get_fs_info(start_offset)\n        return fs_info is not None\n\n    def is_wiped(self):\n        # Image is considered wiped if no volume info, no filesystem detected\n        return self.is_wiped_image\n\n    @property\n    def partitions(self):\n        \"\"\"Get partitions with caching.\"\"\"\n        if self._partition_cache is None:\n            self._partition_cache = self._get_partitions()\n        return self._partition_cache\n\n    def get_partitions(self):\n        \"\"\"Retrieve partitions from the loaded image, or indicate unpartitioned space.\"\"\"\n        return self.partitions\n\n    def _get_partitions(self):\n        \"\"\"Internal method to actually retrieve partitions.\"\"\"\n        partitions = []\n        if self.volume_info:\n            for partition in self.volume_info:\n                if not partition.desc:\n                    continue\n                partitions.append((partition.addr, partition.desc, partition.start, partition.len))\n        return partitions\n\n    @lru_cache(maxsize=32)\n    def get_fs_info(self, start_offset):\n        \"\"\"Retrieve the FS_Info for a partition, initializing it if necessary.\"\"\"\n        if start_offset not in self.fs_info_cache:\n            try:\n                fs_info = pytsk3.FS_Info(self.img_info, offset=start_offset * 512)\n                self.fs_info_cache[start_offset] = fs_info\n            except Exception as e:\n                return None\n        return self.fs_info_cache[start_offset]\n\n    @lru_cache(maxsize=32)\n    def get_fs_type(self, start_offset):\n        \"\"\"Retrieve the file system type for a partition.\"\"\"\n        try:\n            fs_type = self.get_fs_info(start_offset).info.ftype\n\n            # Map the file system type to its name\n            fs_type_map = {\n                pytsk3.TSK_FS_TYPE_NTFS: \"NTFS\",\n                pytsk3.TSK_FS_TYPE_FAT12: \"FAT12\",\n                pytsk3.TSK_FS_TYPE_FAT16: \"FAT16\",\n                pytsk3.TSK_FS_TYPE_FAT32: \"FAT32\",\n                pytsk3.TSK_FS_TYPE_EXFAT: \"ExFAT\",\n                pytsk3.TSK_FS_TYPE_EXT2: \"Ext2\",\n                pytsk3.TSK_FS_TYPE_EXT3: \"Ext3\",\n                pytsk3.TSK_FS_TYPE_EXT4: \"Ext4\",\n                pytsk3.TSK_FS_TYPE_ISO9660: \"ISO9660\",\n                pytsk3.TSK_FS_TYPE_HFS: \"HFS\",\n                pytsk3.TSK_FS_TYPE_APFS: \"APFS\"\n            }\n\n            return fs_type_map.get(fs_type, \"Unknown\")\n        except Exception as e:\n            return \"N/A\"\n\n    def check_partition_contents(self, partition_start_offset):\n        \"\"\"Check if a partition has any files or folders.\"\"\"\n        fs = self.get_fs_info(partition_start_offset)\n        if fs:\n            try:\n                root_dir = fs.open_dir(path=\"/\")\n                for _ in root_dir:\n                    return True\n                return False\n            except:\n                return False\n        return False\n\n    def get_directory_contents(self, start_offset, inode_number=None):\n        \"\"\"Get directory contents with caching for performance.\"\"\"\n        cache_key = f\"{start_offset}_{inode_number}\"\n\n        # Check if we have this directory in our cache\n        if cache_key in self._directory_cache:\n            return self._directory_cache[cache_key]\n\n        fs = self.get_fs_info(start_offset)\n        if fs:\n            try:\n                directory = fs.open_dir(inode=inode_number) if inode_number else fs.open_dir(path=\"/\")\n                entries = []\n\n                for entry in directory:\n                    if entry.info.name.name in [b\".\", b\"..\"]:\n                        continue\n\n                    is_directory = False\n                    if entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:\n                        is_directory = True\n\n                    entries.append({\n                        \"name\": entry.info.name.name.decode('utf-8', errors='replace') if hasattr(entry.info.name,\n                                                                                                  'name') else None,\n                        \"is_directory\": is_directory,\n                        \"inode_number\": entry.info.meta.addr if entry.info.meta else None,\n                        \"size\": entry.info.meta.size if entry.info.meta and entry.info.meta.size is not None else 0,\n                        \"accessed\": safe_datetime(entry.info.meta.atime) if hasattr(entry.info.meta,\n                                                                                    'atime') else \"N/A\",\n                        \"modified\": safe_datetime(entry.info.meta.mtime) if hasattr(entry.info.meta,\n                                                                                    'mtime') else \"N/A\",\n                        \"created\": safe_datetime(entry.info.meta.crtime) if hasattr(entry.info.meta,\n                                                                                    'crtime') else \"N/A\",\n                        \"changed\": safe_datetime(entry.info.meta.ctime) if hasattr(entry.info.meta, 'ctime') else \"N/A\",\n                    })\n\n                # Cache results\n                self._directory_cache[cache_key] = entries\n                return entries\n\n            except Exception as e:\n                # Log the exception for debugging purposes\n                logger.error(f\"Error in get_directory_contents: {e}\")\n                return []\n        return []\n\n    def get_registry_hive(self, fs_info, hive_path):\n        \"\"\"Extract a registry hive from the given filesystem.\"\"\"\n        try:\n            registry_file = fs_info.open(hive_path)\n            hive_data = registry_file.read_random(0, registry_file.info.meta.size)\n            return hive_data\n        except Exception as e:\n            logger.error(f\"Error reading registry hive: {e}\")\n            return None\n\n    def get_windows_version(self, start_offset):\n        \"\"\"Get the Windows version from the SOFTWARE registry hive.\"\"\"\n        fs_info = self.get_fs_info(start_offset)\n        if not fs_info:\n            return None\n\n        # if file system is not ntfs, return unknown OS and exit the function\n        if self.get_fs_type(start_offset) != \"NTFS\":\n            return None\n\n        software_hive_data = self.get_registry_hive(fs_info, \"/Windows/System32/config/SOFTWARE\")\n\n        if not software_hive_data:\n            return None\n\n        # Use a context manager to handle the temporary file\n        with FileSystemUtils.temp_file() as temp_hive_path:\n            try:\n                with open(temp_hive_path, 'wb') as temp_hive:\n                    temp_hive.write(software_hive_data)\n\n                reg = Registry.Registry(temp_hive_path)\n                key = reg.open(\"Microsoft\\\\Windows NT\\\\CurrentVersion\")\n\n                # Helper function to safely get registry values\n                def get_reg_value(reg_key, value_name):\n                    try:\n                        return reg_key.value(value_name).value()\n                    except Registry.RegistryValueNotFoundException:\n                        return \"N/A\"\n\n                # Fetching registry values\n                product_name = get_reg_value(key, \"ProductName\")\n                current_version = get_reg_value(key, \"CurrentVersion\")\n                current_build = get_reg_value(key, \"CurrentBuild\")\n                registered_owner = get_reg_value(key, \"RegisteredOwner\")\n                csd_version = get_reg_value(key, \"CSDVersion\")\n                product_id = get_reg_value(key, \"ProductId\")\n\n                return f\"{product_name} Version {current_version}\\nBuild {current_build} {csd_version}\\nOwner: {registered_owner}\\nProduct ID: {product_id}\"\n\n            except Exception as e:\n                logger.error(f\"Error parsing SOFTWARE hive: {e}\")\n                return \"Error in parsing OS version\"\n\n    def read_unallocated_space(self, start_offset, end_offset):\n        try:\n            start_byte_offset = start_offset * SECTOR_SIZE\n            end_byte_offset = max(end_offset * SECTOR_SIZE, start_byte_offset + SECTOR_SIZE - 1)\n            size_in_bytes = end_byte_offset - start_byte_offset + 1  # Ensuring at least some data is read\n\n            if size_in_bytes <= 0:\n                logger.warning(\"Invalid size for unallocated space, adjusting to read at least one sector.\")\n                size_in_bytes = SECTOR_SIZE  # Adjust to read at least one sector\n\n            # For large blocks, read in chunks instead of all at once\n            if size_in_bytes > CHUNK_SIZE:\n                chunks = []\n                for offset in range(start_byte_offset, end_byte_offset, CHUNK_SIZE):\n                    remaining = min(CHUNK_SIZE, end_byte_offset - offset + 1)\n                    chunk = self.img_info.read(offset, remaining)\n                    if not chunk:\n                        break\n                    chunks.append(chunk)\n\n                if not chunks:\n                    return None\n\n                return b''.join(chunks)\n            else:\n                unallocated_space = self.img_info.read(start_byte_offset, size_in_bytes)\n                if unallocated_space is None or len(unallocated_space) == 0:\n                    logger.error(f\"Failed to read unallocated space from offset {start_byte_offset} to {end_byte_offset}\")\n                    return None\n                return unallocated_space\n\n        except Exception as e:\n            logger.error(f\"Error reading unallocated space: {e}\")\n            return None\n\n    def open_image(self):\n        if self.get_image_type() == \"ewf\":\n            filenames = pyewf.glob(self.image_path)\n            ewf_handle = pyewf.handle()\n            ewf_handle.open(filenames)\n            return EWFImgInfo(ewf_handle)\n        else:\n            return pytsk3.Img_Info(self.image_path)\n\n    def list_files(self, extensions=None):\n        \"\"\"Get a list of all files with given extensions.\"\"\"\n        files_list = []\n        img_info = self.open_image()\n\n        try:\n            volume_info = pytsk3.Volume_Info(img_info)\n            for partition in volume_info:\n                if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:\n                    # Store offset in SECTORS (not bytes)\n                    self.process_partition(img_info, partition.start, files_list, extensions)\n        except IOError:\n            self.process_partition(img_info, 0, files_list, extensions)\n\n        return files_list\n\n    def process_partition(self, img_info, offset_sectors, files_list, extensions):\n        \"\"\"Process partition listing - offset_sectors is in sectors, not bytes.\"\"\"\n        try:\n            fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)\n            self._recursive_file_search(fs_info, fs_info.open_dir(path=\"/\"), \"/\", files_list, extensions, None, offset_sectors)\n        except IOError as e:\n            logger.error(f\"Unable to open filesystem at offset {offset_sectors}: {e}\")\n\n    def _recursive_file_search(self, fs_info, directory, parent_path, files_list, extensions, search_query=None, start_offset=0):\n        \"\"\"Recursively search for files in a directory.\"\"\"\n        for entry in directory:\n            if entry.info.name.name in [b\".\", b\"..\"]:\n                continue\n\n            try:\n                file_name = entry.info.name.name.decode(\"utf-8\", errors='replace')\n                file_extension = os.path.splitext(file_name)[1].lower()\n\n                # Determine if this entry should be included in results\n                is_directory = entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR\n\n                if search_query:\n                    # If there's a search query, check if the file name contains the query\n                    if search_query.startswith('.'):\n                        # If the search query is an extension (e.g., '.jpg')\n                        query_matches = file_extension == search_query.lower()\n                        match_reason = f\"extension matches '{search_query}'\" if query_matches else \"\"\n                    else:\n                        # If the search query is a file name or part of it (SUBSTRING MATCH)\n                        query_matches = search_query.lower() in file_name.lower()\n                        match_reason = f\"filename contains '{search_query}'\" if query_matches else \"\"\n                else:\n                    # If no search query, handle based on extensions\n                    if is_directory:\n                        # Always include directories when no search query (for navigation)\n                        query_matches = True\n                        match_reason = \"directory (no filter)\"\n                    else:\n                        # For files, apply extension filter\n                        query_matches = extensions is None or file_extension in extensions or '' in extensions\n                        match_reason = \"extension filter\"\n\n                if is_directory:\n                    # If directory matches search query, add it to results\n                    if query_matches:\n                        dir_info = self._get_directory_metadata(entry, parent_path, start_offset)\n                        files_list.append(dir_info)\n                        if logger.isEnabledFor(logging.DEBUG):\n                            logger.debug(f\"MATCH (DIR): '{file_name}' - {match_reason}\")\n\n                    # Recursively search subdirectory\n                    try:\n                        sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)\n                        self._recursive_file_search(fs_info, sub_directory, os.path.join(parent_path, file_name),\n                                                    files_list,\n                                                    extensions, search_query, start_offset)\n                    except IOError as e:\n                        logger.error(f\"Unable to open directory: {e}\")\n\n                elif entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_REG and query_matches:\n                    file_info = self._get_file_metadata(entry, parent_path, start_offset)\n                    files_list.append(file_info)\n                    if logger.isEnabledFor(logging.DEBUG):\n                        logger.debug(f\"MATCH (FILE): '{file_name}' - {match_reason}\")\n            except UnicodeDecodeError:\n                continue  # Skip entries with encoding issues\n\n    def _get_directory_metadata(self, entry, parent_path, start_offset=0):\n        \"\"\"Get directory metadata for search results.\"\"\"\n        try:\n            dir_name = entry.info.name.name.decode(\"utf-8\", errors='replace')\n            inode_number = entry.info.meta.addr if entry.info.meta else 0\n\n            # Get volume name for this offset\n            volume_name = self._get_volume_name_for_offset(start_offset)\n            # Create full path with volume information\n            full_path = f\"{volume_name}:{os.path.join(parent_path, dir_name)}\"\n\n            return {\n                \"name\": dir_name,\n                \"path\": full_path,\n                \"size\": 0,  # Directories don't have a size in this context\n                \"accessed\": safe_datetime(entry.info.meta.atime if entry.info.meta else None),\n                \"modified\": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),\n                \"created\": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),\n                \"changed\": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),\n                \"inode_item\": str(inode_number),\n                \"inode_number\": inode_number,\n                \"start_offset\": start_offset,\n                \"is_directory\": True,  # Mark as directory\n                \"type\": \"directory\"\n            }\n        except Exception as e:\n            logger.error(f\"Error getting directory metadata: {e}\")\n            return {\n                \"name\": \"Error reading directory\",\n                \"path\": parent_path + \"/unknown\",\n                \"size\": 0,\n                \"accessed\": \"N/A\",\n                \"modified\": \"N/A\",\n                \"created\": \"N/A\",\n                \"changed\": \"N/A\",\n                \"inode_item\": \"0\",\n                \"inode_number\": 0,\n                \"start_offset\": start_offset,\n                \"is_directory\": True,\n                \"type\": \"directory\"\n            }\n\n    def _get_volume_name_for_offset(self, start_offset):\n        \"\"\"Get the volume name (e.g., 'vol0', 'vol1') for a given partition offset.\"\"\"\n        try:\n            partitions = self.get_partitions()\n            for addr, desc, start, length in partitions:\n                if start == start_offset:\n                    return f\"vol{addr}\"\n            # If not found in partitions, it might be a single filesystem image\n            return \"vol0\"\n        except Exception as e:\n            logger.warning(f\"Could not determine volume name for offset {start_offset}: {e}\")\n            return \"vol0\"\n\n    def _get_file_metadata(self, entry, parent_path, start_offset=0):\n        \"\"\"Get file metadata including all fields needed for viewing.\"\"\"\n        try:\n            file_name = entry.info.name.name.decode(\"utf-8\", errors='replace')\n            inode_number = entry.info.meta.addr if entry.info.meta else 0\n\n            # Get volume name for this offset\n            volume_name = self._get_volume_name_for_offset(start_offset)\n            # Create full path with volume information\n            full_path = f\"{volume_name}:{os.path.join(parent_path, file_name)}\"\n\n            return {\n                \"name\": file_name,\n                \"path\": full_path,  # Now includes volume information\n                \"size\": entry.info.meta.size if entry.info.meta else 0,\n                \"accessed\": safe_datetime(entry.info.meta.atime if entry.info.meta else None),\n                \"modified\": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),\n                \"created\": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),\n                \"changed\": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),\n                \"inode_item\": str(inode_number),  # For display compatibility\n                \"inode_number\": inode_number,  # For file content retrieval\n                \"start_offset\": start_offset,  # Partition offset needed for retrieval\n                \"is_directory\": False,  # This method only called for files\n                \"type\": \"file\"  # For compatibility with viewer logic\n            }\n        except Exception as e:\n            logger.error(f\"Error getting file metadata: {e}\")\n            # Return basic info when we encounter errors\n            return {\n                \"name\": \"Error reading file\",\n                \"path\": parent_path + \"/unknown\",\n                \"size\": 0,\n                \"accessed\": \"N/A\",\n                \"modified\": \"N/A\",\n                \"created\": \"N/A\",\n                \"changed\": \"N/A\",\n                \"inode_item\": \"0\",\n                \"inode_number\": 0,\n                \"start_offset\": start_offset,\n                \"is_directory\": False,\n                \"type\": \"file\"\n            }\n\n    def search_files(self, search_query=None):\n        logger.info(f\"ImageHandler.search_files called with query: '{search_query}'\")\n        files_list = []\n        img_info = self.open_image()\n\n        try:\n            volume_info = pytsk3.Volume_Info(img_info)\n            partition_count = 0\n            for partition in volume_info:\n                if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:\n                    partition_count += 1\n                    logger.info(f\"Searching partition {partition_count} (offset: {partition.start} sectors)\")\n                    # Store offset in SECTORS (not bytes) - get_fs_info will multiply by 512\n                    self.process_partition_search(img_info, partition.start, files_list, search_query)\n            logger.info(f\"Searched {partition_count} allocated partitions\")\n        except IOError as e:\n            # No volume information, attempt to read as a single filesystem\n            logger.info(f\"No volume info, reading as single filesystem: {e}\")\n            self.process_partition_search(img_info, 0, files_list, search_query)\n\n        logger.info(f\"Total files found: {len(files_list)}\")\n        return files_list\n\n    def process_partition_search(self, img_info, offset_sectors, files_list, search_query):\n        \"\"\"Process partition search - offset_sectors is in sectors, not bytes.\"\"\"\n        try:\n            logger.info(f\"Opening filesystem at offset {offset_sectors} sectors ({offset_sectors * SECTOR_SIZE} bytes)\")\n            fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)\n            logger.info(f\"Starting recursive search with query: '{search_query}'\")\n            initial_count = len(files_list)\n            self._recursive_file_search(fs_info, fs_info.open_dir(path=\"/\"), \"/\", files_list, None, search_query, offset_sectors)\n            logger.info(f\"Recursive search complete. Found {len(files_list) - initial_count} files in this partition\")\n        except IOError as e:\n            logger.error(f\"Unable to open file system for search: {e}\")\n\n    def get_file_content(self, inode_number, offset):\n        fs = self.get_fs_info(offset)\n        if not fs:\n            return None, None\n\n        try:\n            file_obj = fs.open_meta(inode=inode_number)\n            if file_obj.info.meta.size == 0:\n                logger.info(\"File has no content or is a special metafile!\")\n                return None, None\n\n            # For large files, read in chunks\n            file_size = file_obj.info.meta.size\n            if file_size > CHUNK_SIZE:\n                chunks = []\n                for chunk_offset in range(0, file_size, CHUNK_SIZE):\n                    chunk_size = min(CHUNK_SIZE, file_size - chunk_offset)\n                    chunk = file_obj.read_random(chunk_offset, chunk_size)\n                    if not chunk:\n                        break\n                    chunks.append(chunk)\n                content = b''.join(chunks)\n            else:\n                # Small file, read all at once\n                content = file_obj.read_random(0, file_size)\n\n            metadata = file_obj.info.meta  # Collect the metadata\n            return content, metadata\n\n        except Exception as e:\n            logger.error(f\"Error reading file: {e}\")\n            return None, None\n\n    # Replace static method assignment with an actual instance method\n    def get_readable_size(self, size_in_bytes):\n        \"\"\"Convert bytes to a human-readable string, wrapper for the static utility method.\"\"\"\n        return FileSystemUtils.get_readable_size(size_in_bytes)\n\n\n# DatabaseManager class with optimization\nclass DatabaseManager:\n    def __init__(self, db_path):\n        self.db_path = db_path\n        self.db_conn = None\n        self._icon_cache = {}  # Cache for icon paths\n        self._connect()\n\n    def _connect(self):\n        \"\"\"Establish a connection to the database with proper error handling.\"\"\"\n        try:\n            self.db_conn = sqlite3_connect(self.db_path)\n            # Enable foreign keys\n            self.db_conn.execute(\"PRAGMA foreign_keys = ON\")\n        except Exception as e:\n            logger.error(f\"Error connecting to database: {e}\")\n            self.db_conn = None\n\n    def __del__(self):\n        \"\"\"Ensure connection is closed when object is destroyed.\"\"\"\n        self.close()\n\n    def close(self):\n        \"\"\"Explicitly close the database connection.\"\"\"\n        if self.db_conn:\n            try:\n                self.db_conn.close()\n                self.db_conn = None\n            except Exception as e:\n                logger.error(f\"Error closing database connection: {e}\")\n\n    def get_icon_path(self, icon_type, identifier):\n        \"\"\"Get icon path with caching for performance.\"\"\"\n        # Check cache first\n        cache_key = f\"{icon_type}_{identifier}\"\n        if cache_key in self._icon_cache:\n            return self._icon_cache[cache_key]\n\n        if not self.db_conn:\n            self._connect()\n            if not self.db_conn:\n                return 'Icons/mimetypes/application-x-zerosize.svg'\n\n        try:\n            c = self.db_conn.cursor()\n            # First, try to get the icon for the specific identifier\n            c.execute(\"SELECT path FROM icons WHERE type = ? AND extention = ?\", (icon_type, identifier))\n            result = c.fetchone()\n\n            # If a specific icon exists for the identifier, cache and return it\n            if result:\n                self._icon_cache[cache_key] = result[0]\n                return result[0]\n\n            # If no specific icon exists, check for default icons\n            if icon_type == 'folder':\n                c.execute(\"SELECT path FROM icons WHERE type = ? AND extention = 'folder'\", (icon_type,))\n                result = c.fetchone()\n                default_path = result[0] if result else 'Icons/mimetypes/application-x-zerosize.svg'\n            else:\n                # Try to find a generic icon for the file type first\n                generic_key = f\"{icon_type}_generic\"\n                if generic_key not in self._icon_cache:\n                    c.execute(\"SELECT path FROM icons WHERE type = ? AND extention = 'generic'\", (icon_type,))\n                    result = c.fetchone()\n                    self._icon_cache[generic_key] = result[\n                        0] if result else 'Icons/mimetypes/application-x-zerosize.svg'\n\n                default_path = self._icon_cache[generic_key]\n\n            # Cache the result before returning\n            self._icon_cache[cache_key] = default_path\n            return default_path\n\n        except Exception as e:\n            logger.error(f\"Error fetching icon: {e}\")\n            return 'Icons/mimetypes/application-x-zerosize.svg'\n        finally:\n            if 'c' in locals():\n                c.close()\n\n\n# ImageManager class with optimizations\nclass ImageManager(QThread):\n    operationCompleted = Signal(bool, str)  # Signal to indicate operation completion\n    showMessage = Signal(str, str)  # Signal to show a message (Title, Content)\n    progressUpdated = Signal(int)  # Signal for progress updates\n\n    def __init__(self):\n        super().__init__()\n        self.operation = None\n        self.image_path = None\n        self.file_name = None\n        self.is_running = False\n        self._process = None\n\n    def __del__(self):\n        self.cleanup_resources()\n\n    def cleanup_resources(self):\n        \"\"\"Clean up any resources used by the image mounting process.\"\"\"\n        if self._process and hasattr(self._process, 'poll') and self._process.poll() is None:\n            try:\n                self._process.terminate()\n                self._process = None\n            except:\n                pass\n\n    def run(self):\n        self.is_running = True\n        system = platform.system()\n\n        try:\n            if self.operation == 'mount' and self.image_path:\n                if system == 'Darwin':  # macOS\n                    self._mount_image_macos()\n                elif system == 'Linux':  # Linux (including Kali)\n                    self._mount_image_linux()\n                elif system == 'Windows':  # Windows\n                    self._mount_image_windows()\n                else:\n                    raise Exception(\"Unsupported Operating System\")\n            elif self.operation == 'dismount':\n                if system == 'Darwin':\n                    self._dismount_image_macos()\n                elif system == 'Linux':\n                    self._dismount_image_linux()\n                elif system == 'Windows':\n                    self._dismount_image_windows()\n                else:\n                    raise Exception(\"Unsupported Operating System\")\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Failed to {self.operation} the image. Error: {e}\")\n        finally:\n            self.is_running = False\n\n    def _mount_image_windows(self):\n        \"\"\"Mount image on Windows using Arsenal Image Mounter.\"\"\"\n        try:\n            aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'\n            if not os.path.exists(aim_path):\n                self.operationCompleted.emit(False, \"Arsenal Image Mounter not found. Please install it.\")\n                return\n\n            cmd = [\n                aim_path,\n                '--mount',\n                '--readonly',\n                f'--filename={self.image_path}'\n            ]\n\n            # Use subprocess.Popen with proper parameter checking\n            self._process = subprocess.Popen(\n                cmd,\n                stdout=subprocess.PIPE,\n                stderr=subprocess.PIPE,\n                creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0\n            )\n\n            # Wait for the process to complete or timeout after 30 seconds\n            try:\n                stdout, stderr = self._process.communicate(timeout=MOUNT_TIMEOUT)\n                if self._process.returncode != 0:\n                    error_msg = stderr.decode('utf-8', errors='replace')\n                    self.operationCompleted.emit(False, f\"Failed to mount the image: {error_msg}\")\n                    return\n                self.operationCompleted.emit(True, f\"Image {self.file_name} mounted successfully.\")\n            except subprocess.TimeoutExpired:\n                # Process is taking too long, but this is sometimes normal for mounting\n                # We'll assume it's working in the background\n                self.operationCompleted.emit(True,\n                                             f\"Image {self.file_name} mount initiated. Check Windows Disk Management.\")\n\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Failed to mount the image on Windows. Error: {e}\")\n\n    def _mount_image_macos(self):\n        \"\"\"Mount image on macOS using hdiutil.\"\"\"\n        try:\n            # Step 1: Attach the image without mounting it\n            attach_cmd = [\n                'hdiutil', 'attach',\n                '-imagekey', 'diskimage-class=CRawDiskImage',\n                '-nomount', self.image_path\n            ]\n\n            attach_process = subprocess.Popen(\n                attach_cmd,\n                stdout=subprocess.PIPE,\n                stderr=subprocess.STDOUT\n            )\n\n            # Wait with timeout\n            try:\n                attach_output, _ = attach_process.communicate(timeout=MOUNT_TIMEOUT)\n                if attach_process.returncode != 0:\n                    self.operationCompleted.emit(False, f\"Failed to attach image: {attach_output.decode()}\")\n                    return\n            except subprocess.TimeoutExpired:\n                attach_process.kill()\n                self.operationCompleted.emit(False, \"Attaching image timed out\")\n                return\n\n            attach_output = attach_output.decode().strip()\n\n            # Step 2: Add a short delay to ensure the system has time to process the attachment\n            QThread.msleep(THREAD_SLEEP_MS)  # More reliable than time.sleep in a QThread\n\n            # Step 3: Extract the disk identifier from the output\n            lines = attach_output.splitlines()\n            disk_identifier = None\n\n            for line in lines:\n                if line.startswith('/dev/disk'):\n                    disk_identifier = line.split()[0]\n                    break\n\n            if not disk_identifier:\n                self.operationCompleted.emit(False, \"Failed to find disk identifier after attaching the image.\")\n                return\n\n            # Step 4: Mount the disk using the identifier\n            mount_cmd = ['hdiutil', 'mount', disk_identifier]\n            mount_process = subprocess.Popen(\n                mount_cmd,\n                stdout=subprocess.PIPE,\n                stderr=subprocess.STDOUT\n            )\n\n            try:\n                mount_output, _ = mount_process.communicate(timeout=MOUNT_TIMEOUT)\n                if mount_process.returncode != 0:\n                    self.operationCompleted.emit(False, f\"Failed to mount disk: {mount_output.decode()}\")\n                    return\n            except subprocess.TimeoutExpired:\n                mount_process.kill()\n                self.operationCompleted.emit(False, \"Mounting timed out\")\n                return\n\n            mount_output = mount_output.decode().strip()\n\n            # Step 5: Extract the mount point (e.g., /Volumes/LABEL2)\n            lines = mount_output.splitlines()\n            mount_point = None\n\n            for line in lines:\n                if line.startswith('/dev/') and '\\t' in line:\n                    mount_point = line.split('\\t')[1]\n                    break\n\n            if mount_point:\n                # Emit success with the mount point\n                self.operationCompleted.emit(True, f\"Image {self.file_name} mounted successfully at {mount_point}.\")\n            else:\n                self.operationCompleted.emit(False, f\"Image {self.file_name} mounted, but no volumes were detected.\")\n\n        except subprocess.CalledProcessError as e:\n            self.operationCompleted.emit(False, f\"Failed to mount the image on macOS. Error: {e.output.decode()}\")\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Unexpected error mounting image: {str(e)}\")\n\n    def _mount_image_linux(self):\n        \"\"\"Mount image on Linux using appropriate tools.\"\"\"\n        try:\n            if self.image_path.lower().endswith('.e01'):\n                # Use ewfmount for .e01 images\n                ewf_mount_dir = '/mnt/ewf'\n\n                # Create mount directory if it doesn't exist\n                if not os.path.exists(ewf_mount_dir):\n                    os.makedirs(ewf_mount_dir, exist_ok=True)\n\n                # Run ewfmount with proper error handling\n                ewf_cmd = ['sudo', 'ewfmount', self.image_path, ewf_mount_dir]\n                ewf_process = subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)\n\n                # Get the partition table info using fdisk\n                fdisk_cmd = ['fdisk', '-l', os.path.join(ewf_mount_dir, 'ewf1')]\n                fdisk_output = subprocess.check_output(fdisk_cmd, text=True)\n\n                # Find the partition start sector\n                partition_start_sector = None\n                for line in fdisk_output.splitlines():\n                    if '/dev/' in line and not line.startswith('Disk '):\n                        # Assuming you want the first partition listed\n                        parts = line.split()\n                        if len(parts) > 1:\n                            try:\n                                partition_start_sector = int(parts[1])\n                                break\n                            except (ValueError, IndexError):\n                                continue\n\n                if partition_start_sector is None:\n                    raise Exception(\"Failed to find partition start sector in the EWF image.\")\n\n                # Calculate the byte offset\n                byte_offset = partition_start_sector * 512\n\n                # Mount the partition using the calculated offset\n                mount_dir = '/mnt/disk_image'\n                os.makedirs(mount_dir, exist_ok=True)\n\n                mount_cmd = [\n                    'sudo', 'mount', '-o',\n                    f'ro,loop,offset={byte_offset}',\n                    os.path.join(ewf_mount_dir, 'ewf1'),\n                    mount_dir\n                ]\n\n                mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)\n\n            else:\n                # Use mount for .dd images and other raw formats\n                mount_dir = '/mnt/disk_image'\n                os.makedirs(mount_dir, exist_ok=True)\n\n                mount_cmd = [\n                    'sudo', 'mount', '-o', 'loop,ro',\n                    self.image_path, mount_dir\n                ]\n\n                mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)\n\n            self.operationCompleted.emit(True, f\"Image {self.file_name} mounted successfully.\")\n        except subprocess.CalledProcessError as e:\n            self.operationCompleted.emit(False, f\"Failed to mount the image on Linux. Error: {e.stderr}\")\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"An unexpected error occurred: {str(e)}\")\n\n    def _dismount_image_linux(self):\n        \"\"\"Dismount image on Linux.\"\"\"\n        try:\n            # Attempt to unmount the disk image\n            disk_cmd = ['sudo', 'umount', '/mnt/disk_image']\n            ewf_cmd = ['sudo', 'umount', '/mnt/ewf']\n\n            try:\n                # Try to unmount disk image\n                subprocess.run(disk_cmd, check=True, capture_output=True, text=True)\n            except subprocess.CalledProcessError as e:\n                logger.warning(f\"Could not unmount disk image: {e.stderr}\")\n\n            try:\n                # Try to unmount EWF\n                subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)\n            except subprocess.CalledProcessError as e:\n                logger.warning(f\"Could not unmount EWF: {e.stderr}\")\n\n            self.operationCompleted.emit(True, \"Image was dismounted successfully.\")\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Failed to dismount the image on Linux. Error: {str(e)}\")\n\n    def _dismount_image_macos(self):\n        \"\"\"Dismount image on macOS using hdiutil.\"\"\"\n        try:\n            # Get the list of currently mounted disk images\n            info_cmd = ['hdiutil', 'info']\n            info_process = subprocess.Popen(\n                info_cmd,\n                stdout=subprocess.PIPE,\n                stderr=subprocess.STDOUT\n            )\n\n            try:\n                info_output, _ = info_process.communicate(timeout=INFO_TIMEOUT)\n                if info_process.returncode != 0:\n                    self.operationCompleted.emit(False, f\"Failed to get mounted disks: {info_output.decode()}\")\n                    return\n            except subprocess.TimeoutExpired:\n                info_process.kill()\n                self.operationCompleted.emit(False, \"Getting disk info timed out\")\n                return\n\n            info_output = info_output.decode()\n\n            lines = info_output.splitlines()\n            mounted_disks = []\n            current_image_path = None\n\n            # Parse the output to find the disk identifier for the given image path\n            for line in lines:\n                if 'image-path' in line:\n                    current_image_path = line.split(': ')[1].strip()\n                elif line.startswith('/dev/disk') and current_image_path == self.image_path:\n                    disk_identifier = line.split()[0]\n                    mounted_disks.append(disk_identifier)\n                    current_image_path = None  # Reset after finding the corresponding disk\n\n            if not mounted_disks:\n                # If we're not targeting a specific image, try to unmount all mounted disks\n                if not self.image_path:\n                    for line in lines:\n                        if line.startswith('/dev/disk'):\n                            disk_identifier = line.split()[0]\n                            mounted_disks.append(disk_identifier)\n\n                if not mounted_disks:\n                    self.operationCompleted.emit(False, \"No mounted images found.\")\n                    return\n\n            # Attempt to dismount all found disk identifiers\n            success = False\n            errors = []\n\n            for disk_identifier in mounted_disks:\n                try:\n                    detach_cmd = ['hdiutil', 'detach', disk_identifier]\n                    detach_process = subprocess.run(detach_cmd, check=True, capture_output=True, text=True)\n                    success = True\n                except subprocess.CalledProcessError:\n                    try:\n                        # If normal detach fails, attempt a forced detach\n                        force_detach_cmd = ['hdiutil', 'detach', '-force', disk_identifier]\n                        force_process = subprocess.run(force_detach_cmd, check=True, capture_output=True, text=True)\n                        success = True\n                    except subprocess.CalledProcessError as e:\n                        errors.append(f\"Failed to detach {disk_identifier}: {e.stderr}\")\n\n            if success:\n                self.operationCompleted.emit(True, \"Image was dismounted successfully.\")\n            else:\n                self.operationCompleted.emit(False, \"Failed to dismount all images: \" + \"; \".join(errors))\n\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Failed to dismount the image on macOS: {str(e)}\")\n\n    def _dismount_image_windows(self):\n        \"\"\"Dismount image on Windows using Arsenal Image Mounter.\"\"\"\n        try:\n            aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'\n            if not os.path.exists(aim_path):\n                self.operationCompleted.emit(False, \"Arsenal Image Mounter not found. Please install it.\")\n                return\n\n            cmd = [aim_path, '--dismount']\n\n            # Use subprocess.run with proper error handling\n            process = subprocess.run(\n                cmd,\n                check=True,\n                capture_output=True,\n                text=True,\n                creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0\n            )\n\n            self.operationCompleted.emit(True, \"Image was dismounted successfully.\")\n        except subprocess.CalledProcessError as e:\n            self.operationCompleted.emit(False, f\"Failed to dismount the image on Windows. Error: {e.stderr}\")\n        except Exception as e:\n            self.operationCompleted.emit(False, f\"Unexpected error dismounting image: {str(e)}\")\n\n    def dismount_image(self):\n        \"\"\"Attempt to dismount the currently mounted image.\"\"\"\n        if self.is_running:\n            self.showMessage.emit(\"Operation in Progress\", \"Please wait for the current operation to complete.\")\n            return\n\n        self.operation = 'dismount'\n        self.start()\n\n    def mount_image(self):\n        \"\"\"Attempt to mount an image after prompting the user to select one.\"\"\"\n        if self.is_running:\n            self.showMessage.emit(\"Operation in Progress\", \"Please wait for the current operation to complete.\")\n            return\n\n        system = platform.system()\n\n        if system == 'Darwin':  # macOS\n            # Only allow .raw and .dd files on macOS\n            supported_formats = \"Raw Files (*.raw *.dd);;All Files (*)\"\n            valid_extensions = ['.raw', '.dd']\n        else:\n            # Original behavior for other operating systems\n            supported_formats = (\n                \"EWF Files (*.E01);;Raw Files (*.dd);;AFF4 Files (*.aff4);;\"\n                \"VHD Files (*.vhd);;VDI Files (*.vdi);;XVA Files (*.xva);;\"\n                \"VMDK Files (*.vmdk);;OVA Files (*.ova);;QCOW Files (*.qcow *.qcow2);;All Files (*)\"\n            )\n            valid_extensions = ['.e01', '.dd', '.aff4', '.vhd', '.vdi', '.xva', '.vmdk', '.ova', '.qcow', '.qcow2']\n\n        while True:\n            image_path, _ = QFileDialog.getOpenFileName(QWidget(None), \"Select Disk Image\", \"\", supported_formats)\n\n            if not image_path:\n                return  # No image was selected, so just exit the function\n\n            file_extension = os.path.splitext(image_path)[1].lower()\n            if file_extension in valid_extensions:\n                break  # Exit the loop if a valid image was selected\n            else:\n                # Show an error message for an invalid file\n                QMessageBox.warning(QWidget(None), \"Invalid File Type\", \"The selected file is not a valid disk image.\")\n\n        # Normalize the path\n        self.image_path = os.path.normpath(image_path)\n        self.file_name = os.path.basename(self.image_path)\n        self.operation = 'mount'\n        self.start()\n\n\n# ==================== FILE SEARCH WIDGET CLASSES ====================\nclass SizeTableWidgetItem(QTableWidgetItem):\n    \"\"\"Custom table widget item for proper size sorting.\"\"\"\n    def __lt__(self, other):\n        return int(self.data(Qt.UserRole)) < int(other.data(Qt.UserRole))\n\n\n\n\nclass MainWindow(QMainWindow):\n    # Class variable for icon caching\n    _icon_cache = {}\n\n    def __init__(self):\n        super().__init__()\n\n        # Create a database manager for icon lookup\n        self.db_manager = DatabaseManager('tools/new_database_mappings.db')\n\n        # Initialize variables for tracking\n        self.current_selected_data = None\n        self.current_offset = None\n        self.current_path = \"/\"  # Initialize current path\n        self.image_handler = None\n        self._directory_cache = {}\n\n        # Search/Browse mode state management\n        self._search_mode = False  # False = Browse mode, True = Search mode\n        self._search_query = \"\"  # Current search query\n        self._last_browsed_state = {}  # Store last directory state for restoration\n\n        # Search debounce timer - wait for user to stop typing before searching\n        self._search_timer = QTimer()\n        self._search_timer.setSingleShot(True)\n        self._search_timer.setInterval(500)  # 500ms delay after last keystroke\n        self._search_timer.timeout.connect(self._execute_search)\n\n        # Directory navigation history (for Back/Forward buttons like Windows 11)\n        self._directory_history = []  # List of visited directories: [(offset, inode, path), ...]\n        self._history_index = -1  # Current position in history (-1 = no history)\n        self._navigating_history = False  # Flag to prevent adding to history during Back/Forward\n\n        # Load configuration\n        self.api_keys = configparser.ConfigParser()\n        try:\n            self.api_keys.read('config.ini')\n        except Exception as e:\n            logger.error(f\"Error loading configuration: {e}\")\n\n        # Initialize instance attributes\n        self.image_mounted = False\n        self.current_offset = None\n        self.current_image_path = None\n        self.image_manager = ImageManager()\n        self.current_selected_data = None\n\n        self.evidence_files = []\n\n        # Connect to named method instead of complex lambda\n        self.image_manager.operationCompleted.connect(self._handle_mount_operation_complete)\n\n        self.initialize_ui()\n\n    # ==================== HELPER METHODS ====================\n\n    def _handle_mount_operation_complete(self, success: bool, message: str) -> None:\n        \"\"\"Handle completion of mount/dismount operation.\"\"\"\n        if success:\n            QMessageBox.information(self, \"Image Operation\", message)\n            self.image_mounted = not self.image_mounted\n        else:\n            QMessageBox.critical(self, \"Image Operation\", message)\n\n    def _get_file_icon(self, file_extension: str) -> QIcon:\n        \"\"\"Get icon for file extension with caching.\"\"\"\n        if file_extension not in self._icon_cache:\n            icon_path = self.db_manager.get_icon_path('file', file_extension)\n            self._icon_cache[file_extension] = QIcon(icon_path)\n        return self._icon_cache[file_extension]\n\n    def _format_partition_text(self, addr: int, desc: bytes, start: int, end: int, length: int, fs_type: str) -> str:\n        \"\"\"Format partition display text.\"\"\"\n        size_in_bytes = length * SECTOR_SIZE\n        readable_size = self.image_handler.get_readable_size(size_in_bytes)\n        desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc\n        return f\"vol{addr} ({desc_str}: {start}-{end}, Size: {readable_size}, FS: {fs_type})\"\n\n    def _confirm_exit(self) -> bool:\n        \"\"\"Ask user to confirm exit.\"\"\"\n        reply = QMessageBox.question(\n            self, 'Exit Confirmation',\n            'Are you sure you want to exit?',\n            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,\n            QMessageBox.StandardButton.No\n        )\n        return reply == QMessageBox.StandardButton.Yes\n\n    def _handle_dismount_if_needed(self) -> None:\n        \"\"\"Dismount image if mounted and user confirms.\"\"\"\n        if not self.image_mounted:\n            return\n\n        reply = QMessageBox.question(\n            self, 'Dismount Image',\n            'Do you want to dismount the mounted image before exiting?',\n            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,\n            QMessageBox.StandardButton.Yes\n        )\n\n        if reply == QMessageBox.StandardButton.Yes:\n            self.image_manager.dismount_image()\n\n\n    def _create_tree_item_for_entry(self, parent_item: QTreeWidgetItem, entry: Dict[str, Any],\n                                    start_offset: int) -> QTreeWidgetItem:\n        \"\"\"Create tree item for a directory entry.\"\"\"\n        child_item = QTreeWidgetItem(parent_item)\n        child_item.setText(0, entry[\"name\"])\n\n        if entry[\"is_directory\"]:\n            self._setup_directory_tree_item(child_item, entry, start_offset)\n        else:\n            self._setup_file_tree_item(child_item, entry, start_offset)\n\n        return child_item\n\n    def _setup_directory_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],\n                                   start_offset: int) -> None:\n        \"\"\"Configure tree item for a directory entry.\"\"\"\n        # Check if directory has children\n        sub_entries = self.image_handler.get_directory_contents(start_offset, entry[\"inode_number\"])\n        has_sub_entries = bool(sub_entries)\n\n        # Set directory icon and data\n        icon_path = self.db_manager.get_icon_path('folder', 'folder')\n        item.setIcon(0, QIcon(icon_path))\n        item.setData(0, Qt.UserRole, {\n            \"inode_number\": entry[\"inode_number\"],\n            \"type\": 'directory',\n            \"start_offset\": start_offset,\n            \"name\": entry[\"name\"]\n        })\n\n        # Set child indicator\n        item.setChildIndicatorPolicy(\n            QTreeWidgetItem.ShowIndicator if has_sub_entries\n            else QTreeWidgetItem.DontShowIndicatorWhenChildless\n        )\n\n    def _setup_file_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],\n                             start_offset: int) -> None:\n        \"\"\"Configure tree item for a file entry.\"\"\"\n        # Get file extension for icon\n        file_extension = entry[\"name\"].split('.')[-1].lower() if '.' in entry[\"name\"] else 'unknown'\n\n        # Use cached icon lookup\n        icon = self._get_file_icon(file_extension)\n        item.setIcon(0, icon)\n        item.setData(0, Qt.UserRole, {\n            \"inode_number\": entry[\"inode_number\"],\n            \"type\": 'file',\n            \"start_offset\": start_offset,\n            \"name\": entry[\"name\"]\n        })\n\n    def _populate_table_entry(self, row_position: int, entry: Dict[str, Any], offset: int) -> None:\n        \"\"\"Populate a single table row with entry data.\"\"\"\n        entry_name = entry.get(\"name\", \"\")\n        inode_number = entry.get(\"inode_number\", 0)\n        is_directory = entry.get(\"is_directory\", False)\n        description = \"Dir\" if is_directory else \"File\"\n        size_in_bytes = entry.get(\"size\", 0)\n        readable_size = self.image_handler.get_readable_size(size_in_bytes)\n        created = entry.get(\"created\", \"N/A\")\n        accessed = entry.get(\"accessed\", \"N/A\")\n        modified = entry.get(\"modified\", \"N/A\")\n        changed = entry.get(\"changed\", \"N/A\")\n\n        icon_type = 'folder' if is_directory else 'file'\n        icon_name = 'folder' if is_directory else (\n            entry_name.split('.')[-1].lower() if '.' in entry_name else 'unknown')\n\n        parent_inode = self.current_selected_data.get(\"inode_number\") if self.current_selected_data else None\n\n        self.listing_table.insertRow(row_position)\n        self.insert_row_into_listing_table(entry_name, inode_number, description,\n                                          icon_name, icon_type, offset,\n                                          readable_size, created, accessed,\n                                          modified, changed, parent_inode)\n\n    # ==================== END HELPER METHODS ====================\n\n    def initialize_ui(self):\n        self.setWindowTitle('Trace 1.2.0')\n\n        # Set application icon for all platforms\n        app_icon = QIcon('Icons/logo_prev_ui.png')\n        self.setWindowIcon(app_icon)\n\n        # Set taskbar/dock icon for different platforms\n        if os.name == 'nt':  # Windows\n            import ctypes\n            myappid = 'Trace'\n            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)\n        else:  # macOS and Linux\n            # For macOS and Linux, setting the app icon at application level\n            QApplication.instance().setWindowIcon(app_icon)\n\n        self.setGeometry(DEFAULT_WINDOW_X, DEFAULT_WINDOW_Y, DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)\n\n        menu_bar = QMenuBar(self)\n        file_actions = {\n            'Add Evidence File': self.load_image_evidence,\n            'Remove Evidence File': self.remove_image_evidence,\n            'Image Mounting': self.image_manager.mount_image,\n            'Image Unmounting': self.image_manager.dismount_image,\n            'separator': None,  # This will add a separator\n            'Exit': self.close\n        }\n\n        self.create_menu(menu_bar, 'File', file_actions)\n\n        view_menu = QMenu('View', self)\n\n        # Create the \"Full Screen\" action and connect it to the showFullScreen slot\n        full_screen_action = QAction(\"Full Screen\", self)\n        full_screen_action.triggered.connect(self.showFullScreen)\n        view_menu.addAction(full_screen_action)\n\n        # Create the \"Normal Screen\" action and connect it to the showNormal slot\n        normal_screen_action = QAction(\"Normal Screen\", self)\n        normal_screen_action.triggered.connect(self.showNormal)\n        view_menu.addAction(normal_screen_action)\n\n        # Add a separator\n        view_menu.addSeparator()\n\n        # **Add Theme Selection Actions**\n        # Create an action group for themes\n        theme_group = QActionGroup(self)\n        theme_group.setExclusive(True)  # Only one theme can be selected at a time\n\n        # Light Theme Action\n        light_theme_action = QAction(\"Light Mode\", self)\n        light_theme_action.setCheckable(True)\n        light_theme_action.setChecked(True)  # Set Light Theme as default\n        light_theme_action.triggered.connect(lambda: self.apply_stylesheet('light'))\n        theme_group.addAction(light_theme_action)\n        view_menu.addAction(light_theme_action)\n\n        # Dark Theme Action\n        dark_theme_action = QAction(\"Dark Mode\", self)\n        dark_theme_action.setCheckable(True)\n        dark_theme_action.triggered.connect(lambda: self.apply_stylesheet('dark'))\n        theme_group.addAction(dark_theme_action)\n        view_menu.addAction(dark_theme_action)\n\n        # Add the view menu to the menu bar\n        menu_bar.addMenu(view_menu)\n\n        # **Apply the default stylesheet**\n        self.apply_stylesheet('light')\n\n        tools_menu = QMenu('Tools', self)\n\n        verify_image_action = QAction(\"Verify Image\", self)\n        verify_image_action.triggered.connect(self.verify_image)\n        tools_menu.addAction(verify_image_action)\n\n        conversion_action = QAction(\"Convert E01 to DD/RAW\", self)\n        conversion_action.triggered.connect(self.show_conversion_widget)\n        tools_menu.addAction(conversion_action)\n\n        veriphone_api_action = QAction(\"Veriphone API\", self)\n        veriphone_api_action.triggered.connect(self.show_veriphone_widget)\n        tools_menu.addAction(veriphone_api_action)\n\n        # Add \"Options\" menu for API key configuration\n        options_menu = QMenu('Options', self)\n        api_key_action = QAction(\"API Keys\", self)\n        api_key_action.triggered.connect(self.show_api_key_dialog)\n        options_menu.addAction(api_key_action)\n\n        help_menu = QMenu('Help', self)\n        help_menu.addAction(\"About\")\n        help_menu.triggered.connect(lambda: AboutDialog(self).exec_())\n\n        menu_bar.addMenu(view_menu)\n        menu_bar.addMenu(tools_menu)\n        menu_bar.addMenu(options_menu)\n        menu_bar.addMenu(help_menu)\n\n        self.setMenuBar(menu_bar)\n\n        self.main_toolbar = QToolBar()\n        self.main_toolbar.setMovable(False)\n        self.main_toolbar.setFloatable(False)\n        self.main_toolbar.addAction(\n            self.create_action('Icons/icons8-evidence-48.png', \"Load Image\", self.load_image_evidence))\n        self.main_toolbar.addAction(\n            self.create_action('Icons/icons8-evidence-96.png', \"Remove Image\", self.remove_image_evidence))\n        self.main_toolbar.addSeparator()\n\n        # Create verify_image_button as an attribute of MainWindow\n        self.verify_image_button = self.create_action('Icons/icons8-verify-blue.png', \"Verify Image\", self.verify_image)\n        self.main_toolbar.addAction(self.verify_image_button)\n\n        self.main_toolbar.addSeparator()\n        self.main_toolbar.addAction(\n            self.create_action('Icons/devices/icons8-hard-disk-48.png', \"Mount Image\", self.image_manager.mount_image))\n        self.main_toolbar.addAction(self.create_action('Icons/devices/icons8-hard-disk-48_red.png', \"Unmount Image\",\n                                                       self.image_manager.dismount_image))\n\n        # Navigation buttons (Back, Forward, Up) will be added to the listing search toolbar\n        # Created later in the UI setup\n\n        self.addToolBar(Qt.TopToolBarArea, self.main_toolbar)\n\n        self.tree_viewer = QTreeWidget(self)\n        self.tree_viewer.setIconSize(QSize(16, 16))\n        self.tree_viewer.setHeaderHidden(True)\n        self.tree_viewer.itemExpanded.connect(self.on_item_expanded)\n        self.tree_viewer.itemClicked.connect(self.on_item_clicked)\n        self.tree_viewer.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.tree_viewer.customContextMenuRequested.connect(self.open_tree_context_menu)\n\n        tree_dock = QDockWidget('Tree View', self)\n\n        tree_dock.setWidget(self.tree_viewer)\n        self.addDockWidget(Qt.LeftDockWidgetArea, tree_dock)\n\n        self.result_viewer = QTabWidget(self)\n        self.setCentralWidget(self.result_viewer)\n\n        self.listing_table = QTableWidget()\n        self.listing_table.setSortingEnabled(True)\n        self.listing_table.verticalHeader().setVisible(False)\n        self.listing_table.setObjectName(\"listingTable\")  # Set object name for specific CSS styling\n\n        # Set size policy to expand with window\n        self.listing_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n        # Use alternate row colors\n        self.listing_table.setAlternatingRowColors(True)\n        self.listing_table.setEditTriggers(QTableWidget.NoEditTriggers)\n        self.listing_table.setIconSize(QSize(24, 24))\n        self.listing_table.setColumnCount(10)  # 10 columns: Name, Inode, Type, Size, 4 timestamps, Path, Info\n\n        # Enable horizontal scrolling for smaller windows\n        self.listing_table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)\n        self.listing_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n\n        # Connect click event to handle navigation in search mode\n        self.listing_table.itemClicked.connect(self.on_listing_table_item_clicked)\n\n        # Create a QVBoxLayout for the listing tab\n        self.listing_layout = QVBoxLayout()\n        self.listing_layout.setContentsMargins(0, 0, 0, 0)  # Set to zero to remove margins\n        self.listing_layout.setSpacing(0)  # Remove spacing between widgets\n\n        # ==================== CREATE UNIFIED TOOLBAR (like File Carving tab) ====================\n        self.listing_toolbar = QToolBar()\n        self.listing_toolbar.setContentsMargins(0, 0, 0, 0)\n        self.listing_toolbar.setMovable(False)\n\n        # LEFT SIDE: Icon and Title\n        self.listing_icon_label = QLabel()\n        self.listing_icon_label.setPixmap(QPixmap('Icons/icons8-search-in-browser-50.png'))\n        self.listing_icon_label.setFixedSize(48, 48)\n        self.listing_toolbar.addWidget(self.listing_icon_label)\n\n        self.listing_title_label = QLabel(\"File System Browser\")\n        self.listing_title_label.setStyleSheet(\"\"\"\n            QLabel {\n                font-size: 20px;\n                color: #37c6d0;\n                font-weight: bold;\n                margin-left: 8px;\n            }\n        \"\"\")\n        self.listing_toolbar.addWidget(self.listing_title_label)\n\n        # Add spacer after title\n        title_spacer = QLabel()\n        title_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.listing_toolbar.addWidget(title_spacer)\n\n        # MIDDLE: Navigation buttons (Back, Forward, Up) - next to title\n        self.back_action = QAction(QIcon(\"Icons/icons8-left-arrow-50.png\"), \"Back\", self)\n        self.back_action.triggered.connect(self.navigate_back)\n        self.back_action.setEnabled(False)\n        self.listing_toolbar.addAction(self.back_action)\n\n        self.forward_action = QAction(QIcon(\"Icons/icons8-right-arrow-50.png\"), \"Forward\", self)\n        self.forward_action.triggered.connect(self.navigate_forward)\n        self.forward_action.setEnabled(False)\n        self.listing_toolbar.addAction(self.forward_action)\n\n        self.go_up_action = QAction(QIcon(\"Icons/icons8-thick-arrow-pointing-up-50.png\"), \"Go Up Directory\", self)\n        self.go_up_action.triggered.connect(self.navigate_up_directory)\n        self.go_up_action.setEnabled(False)\n        self.listing_toolbar.addAction(self.go_up_action)\n\n        # Add vertical separator after navigation buttons\n        self.listing_toolbar.addSeparator()\n\n        # RIGHT SIDE: Search functionality\n        # Add search bar\n        self.listing_search_bar = QLineEdit()\n        self.listing_search_bar.setObjectName(\"listingSearchBar\")\n        self.listing_search_bar.setPlaceholderText(\"Search files (press Enter, supports wildcards: *.pdf, name.*)\")\n        self.listing_search_bar.setFixedHeight(35)\n        self.listing_search_bar.setFixedWidth(450)\n        # Only search when user presses Enter\n        self.listing_search_bar.returnPressed.connect(self.trigger_listing_search)\n        # Monitor text changes for auto-clearing results\n        self.listing_search_bar.textChanged.connect(self.on_listing_search_text_changed)\n        self.listing_toolbar.addWidget(self.listing_search_bar)\n\n        # Add small end spacer\n        end_spacer = QWidget()\n        end_spacer.setFixedWidth(10)\n        self.listing_toolbar.addWidget(end_spacer)\n\n        # Add the single toolbar and listing table to the layout\n        self.listing_layout.addWidget(self.listing_toolbar)  # Single unified toolbar\n        self.listing_layout.addWidget(self.listing_table)  # Table below toolbar\n\n        # Create a widget to hold the layout\n        self.listing_widget = QWidget()\n        self.listing_widget.setLayout(self.listing_layout)\n\n        # Set the horizontal header with hybrid resizing approach\n        header = self.listing_table.horizontalHeader()\n\n        # All columns use Interactive mode (fixed width, manually resizable)\n        # This enables horizontal scrolling on smaller windows\n        header.setSectionResizeMode(0, QHeaderView.Interactive)  # Name - fixed, manually resizable\n        header.setSectionResizeMode(1, QHeaderView.Interactive)  # Inode - fixed, manually resizable\n        header.setSectionResizeMode(2, QHeaderView.Interactive)  # Type - fixed, manually resizable\n        header.setSectionResizeMode(3, QHeaderView.Interactive)  # Size - fixed, manually resizable\n        header.setSectionResizeMode(4, QHeaderView.Interactive)  # Created - fixed, manually resizable\n        header.setSectionResizeMode(5, QHeaderView.Interactive)  # Accessed - fixed, manually resizable\n        header.setSectionResizeMode(6, QHeaderView.Interactive)  # Modified - fixed, manually resizable\n        header.setSectionResizeMode(7, QHeaderView.Interactive)  # Changed - fixed, manually resizable\n        header.setSectionResizeMode(8, QHeaderView.Interactive)  # Path - fixed, manually resizable\n        header.setSectionResizeMode(9, QHeaderView.Interactive)  # Info - fixed, manually resizable\n\n        # Set initial column widths\n        self.listing_table.setColumnWidth(0, COLUMN_WIDTHS['name'])      # Name - 400px (widest)\n        self.listing_table.setColumnWidth(1, COLUMN_WIDTHS['inode'])     # Inode - 45px\n        self.listing_table.setColumnWidth(2, COLUMN_WIDTHS['type'])      # Type - 50px\n        self.listing_table.setColumnWidth(3, COLUMN_WIDTHS['size'])      # Size - 70px\n        self.listing_table.setColumnWidth(4, COLUMN_WIDTHS['created'])   # Created - 90px (narrower)\n        self.listing_table.setColumnWidth(5, COLUMN_WIDTHS['accessed'])  # Accessed - 90px (narrower)\n        self.listing_table.setColumnWidth(6, COLUMN_WIDTHS['modified'])  # Modified - 90px (narrower)\n        self.listing_table.setColumnWidth(7, COLUMN_WIDTHS['changed'])   # Changed - 90px (narrower)\n        self.listing_table.setColumnWidth(8, COLUMN_WIDTHS['path'])      # Path - 300px (wide)\n        self.listing_table.setColumnWidth(9, 250)                        # Info - 250px (for volumes)\n\n        # Remove any extra space in the header\n        header.setStyleSheet(\"QHeaderView::section { margin-top: 0px; padding-top: 2px; }\")\n        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)\n\n        # Set the header labels\n        self.listing_table.setHorizontalHeaderLabels(\n            ['Name', 'Inode', 'Type', 'Size', 'Created Date', 'Accessed Date', 'Modified Date', 'Changed Date', 'Path', 'Info']\n        )\n\n        self.listing_table.itemDoubleClicked.connect(self.on_listing_table_item_clicked)\n        self.listing_table.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.listing_table.customContextMenuRequested.connect(self.open_listing_context_menu)\n        self.listing_table.setSelectionBehavior(QTableWidget.SelectRows)\n\n        # Set the color of the selected row\n        palette = self.listing_table.palette()\n        palette.setBrush(QPalette.Highlight, QBrush(Qt.lightGray))  # Change Qt.lightGray to your preferred color\n        self.listing_table.setPalette(palette)\n\n        header = self.listing_table.horizontalHeader()\n        header.setDefaultAlignment(Qt.AlignLeft)\n\n        self.result_viewer.addTab(self.listing_widget, 'Listing')\n\n        self.deleted_files_widget = FileCarvingWidget(self)\n        self.result_viewer.addTab(self.deleted_files_widget, 'Deleted Files')\n\n        self.registry_extractor_widget = RegistryExtractor(self.image_handler)\n        self.result_viewer.addTab(self.registry_extractor_widget, 'Registry')\n\n        self.viewer_tab = QTabWidget(self)\n\n        self.hex_viewer = HexViewer(self)\n        self.viewer_tab.addTab(self.hex_viewer, 'Hex')\n\n        self.text_viewer = TextViewer(self)\n        self.viewer_tab.addTab(self.text_viewer, 'Text')\n\n        self.application_viewer = UnifiedViewer(self)\n        self.application_viewer.layout.setContentsMargins(0, 0, 0, 0)\n        self.application_viewer.layout.setSpacing(0)\n        self.viewer_tab.addTab(self.application_viewer, 'Application')\n\n        self.metadata_viewer = MetadataViewer(self.image_handler)\n        self.viewer_tab.addTab(self.metadata_viewer, 'File Metadata')\n\n        self.exif_viewer = ExifViewer(self)\n        self.viewer_tab.addTab(self.exif_viewer, 'Exif Data')\n\n        self.virus_total_api = VirusTotal()\n        self.viewer_tab.addTab(self.virus_total_api, 'Virus Total API')\n\n        # Set the API key if it exists\n        virus_total_key = self.api_keys.get('API_KEYS', 'virustotal', fallback='')\n        self.virus_total_api.set_api_key(virus_total_key)\n\n        self.viewer_dock = QDockWidget('Utils', self)\n        self.viewer_dock.setWidget(self.viewer_tab)\n        self.addDockWidget(Qt.BottomDockWidgetArea, self.viewer_dock)\n\n        self.viewer_dock.setMinimumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)\n        self.viewer_dock.setMaximumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)\n        self.viewer_dock.visibilityChanged.connect(self.on_viewer_dock_focus)\n        self.viewer_tab.currentChanged.connect(self.display_content_for_active_tab)\n\n        # disable all tabs before loading an image file\n        self.enable_tabs(False)\n\n    def apply_stylesheet(self, theme='light'):\n        if theme == 'dark':\n            qss_file = 'styles/dark_theme.qss'\n        else:\n            qss_file = 'styles/light_theme.qss'  # Ensure your existing QSS file is named 'light_theme.qss'\n\n        try:\n            with open(qss_file, 'r') as f:\n                stylesheet = f.read()\n            QApplication.instance().setStyleSheet(stylesheet)\n        except Exception as e:\n            logger.error(f\"Error loading stylesheet {qss_file}: {e}\")\n\n    def show_api_key_dialog(self):\n        # Create a dialog to get API keys from the user\n        dialog = QDialog(self)\n        dialog.setWindowTitle(\"API Key Configuration\")\n        dialog.setFixedWidth(API_DIALOG_WIDTH)  # Set a fixed width to accommodate longer API keys\n\n        # Set layout as a form layout for better presentation\n        layout = QFormLayout()\n        layout.setSpacing(10)  # Add some spacing between fields\n        layout.setContentsMargins(15, 15, 15, 15)  # Set content margins for better visual aesthetics\n\n        # VirusTotal API Key\n        virus_total_label = QLabel(\"VirusTotal API Key:\")\n        virus_total_input = QLineEdit()\n        virus_total_input.setText(self.api_keys.get('API_KEYS', 'virustotal', fallback=''))\n        virus_total_input.setMinimumWidth(INPUT_FIELD_MIN_WIDTH)  # Set a minimum width for the input field\n        layout.addRow(virus_total_label, virus_total_input)\n\n        # Veriphone API Key\n        veriphone_label = QLabel(\"Veriphone API Key:\")\n        veriphone_input = QLineEdit()\n        veriphone_input.setText(self.api_keys.get('API_KEYS', 'veriphone', fallback=''))\n        veriphone_input.setMinimumWidth(INPUT_FIELD_MIN_WIDTH)  # Set a minimum width for the input field\n        layout.addRow(veriphone_label, veriphone_input)\n\n        # Buttons\n        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)\n        button_box.accepted.connect(\n            lambda: self.save_api_keys(virus_total_input.text(), veriphone_input.text(), dialog))\n        button_box.rejected.connect(dialog.reject)\n        layout.addRow(button_box)\n\n        # Set layout and execute dialog\n        dialog.setLayout(layout)\n        dialog.exec_()\n\n    def save_api_keys(self, virus_total_key, veriphone_key, dialog):\n        # Save the API keys in a configuration file\n        if not self.api_keys.has_section('API_KEYS'):\n            self.api_keys.add_section('API_KEYS')\n\n        self.api_keys.set('API_KEYS', 'virustotal', virus_total_key)\n        self.api_keys.set('API_KEYS', 'veriphone', veriphone_key)\n\n        with open('config.ini', 'w') as config_file:\n            self.api_keys.write(config_file)\n\n        dialog.accept()\n\n        # Pass the updated API keys to the appropriate modules\n        self.virus_total_api.set_api_key(virus_total_key)\n\n        # Set Veriphone API key only if the widget is created\n        if hasattr(self, 'veriphone_widget'):\n            self.veriphone_widget.set_api_key(veriphone_key)\n\n    def show_conversion_widget(self):\n        \"\"\"Show the conversion widget.\"\"\"\n        self.select_dialog = Main()\n        self.select_dialog.show()\n\n    def show_veriphone_widget(self):\n        \"\"\"Create the VeriphoneWidget only if it hasn't been created yet.\"\"\"\n        if not hasattr(self, 'veriphone_widget'):\n            self.veriphone_widget = VeriphoneWidget()\n            # Set the API key after creating the widget\n            veriphone_key = self.api_keys.get('API_KEYS', 'veriphone', fallback='')\n            self.veriphone_widget.set_api_key(veriphone_key)\n        self.veriphone_widget.show()\n\n    def verify_image(self):\n        if self.image_handler is None:\n            QMessageBox.warning(self, \"Verify Image\", \"No image is currently loaded.\")\n            return\n\n        # Show the verification widget\n        self.verification_widget = VerificationWidget(self.image_handler)\n\n        # Connect a signal when the verification widget is closed to update the icon\n        self.verification_widget.closeEvent = lambda event: self.on_verification_closed(event)\n\n        # Show the widget\n        self.verification_widget.show()\n\n    def on_verification_closed(self, event):\n        \"\"\"Handle the verification widget being closed.\"\"\"\n        # Make sure verify_image_button exists before trying to change its icon\n        if hasattr(self, 'verify_image_button'):\n            if hasattr(self.verification_widget, 'is_verified') and self.verification_widget.is_verified:\n                self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-48_gren.png'))\n            else:\n                self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-blue.png'))\n\n        # Call the original closeEvent to close the widget\n        QWidget.closeEvent(self.verification_widget, event)\n\n    def enable_tabs(self, state):\n        self.result_viewer.setEnabled(state)\n        self.viewer_tab.setEnabled(state)\n        self.listing_table.setEnabled(state)\n        self.deleted_files_widget.setEnabled(state)\n        self.registry_extractor_widget.setEnabled(state)\n\n    def create_menu(self, menu_bar, menu_name, actions):\n        menu = QMenu(menu_name, self)\n        for action_name, action_function in actions.items():\n            if action_name == 'separator':\n                menu.addSeparator()\n            else:\n                action = menu.addAction(action_name)\n                action.triggered.connect(action_function)\n        menu_bar.addMenu(menu)\n        return menu\n\n    @staticmethod\n    def create_tree_item(parent, text, icon_path, data):\n        item = QTreeWidgetItem(parent)\n        item.setText(0, text)\n        item.setIcon(0, QIcon(icon_path))\n        item.setData(0, Qt.UserRole, data)\n        return item\n\n    def on_viewer_dock_focus(self, visible):\n        if visible:  # If the QDockWidget is focused/visible\n            self.viewer_dock.setMaximumSize(QT_MAX_SIZE, QT_MAX_SIZE)  # Remove size constraints\n        else:  # If the QDockWidget loses focus\n            current_height = self.viewer_dock.size().height()  # Get the current height\n            self.viewer_dock.setMinimumSize(VIEWER_DOCK_MAX_WIDTH, current_height)\n            self.viewer_dock.setMaximumSize(VIEWER_DOCK_MAX_WIDTH, current_height)\n\n    def clear_ui(self):\n        self.listing_table.clearContents()\n        self.listing_table.setRowCount(0)\n        self.clear_viewers()\n        self.current_image_path = None\n        self.current_offset = None\n        self.image_mounted = False\n        self.evidence_files.clear()\n        self.deleted_files_widget.clear()\n\n        # Clear search bar and reset filters\n        self.listing_search_bar.clear()\n\n        # Clear navigation history\n        self._directory_history = []\n        self._history_index = -1\n        self._update_navigation_buttons()\n\n        # Disable directory up button\n        self.go_up_action.setEnabled(False)\n\n    def clear_viewers(self):\n        self.hex_viewer.clear_content()\n        self.text_viewer.clear_content()\n        self.application_viewer.clear()\n        self.metadata_viewer.clear()\n        self.exif_viewer.clear_content()\n        self.registry_extractor_widget.clear()\n\n    def closeEvent(self, event):\n        \"\"\"Handle application close event.\"\"\"\n        if not self._confirm_exit():\n            event.ignore()\n            return\n\n        self._handle_dismount_if_needed()\n\n        # Cleanup resources\n        self.cleanup_resources()\n        event.accept()\n\n    def cleanup_resources(self):\n        \"\"\"Clean up all resources when closing the application.\"\"\"\n        # Clean up application viewer first to ensure media players are properly shut down\n        try:\n            if hasattr(self, 'application_viewer'):\n                if hasattr(self.application_viewer, 'shutdown'):\n                    self.application_viewer.shutdown()\n                else:\n                    self.application_viewer.clear()\n        except Exception as e:\n            logger.error(f\"Error shutting down application viewer: {e}\")\n\n        # Stop any running background operations\n        for attr_name in dir(self):\n            attr = getattr(self, attr_name)\n            # Check if it's a thread and running\n            if isinstance(attr, QThread) and hasattr(attr, 'isRunning') and attr.isRunning():\n                try:\n                    # Try to stop it gracefully\n                    attr.quit()\n                    attr.wait(1000)  # Wait up to 1 second\n\n                    # If still running, terminate it\n                    if attr.isRunning():\n                        attr.terminate()\n                except Exception as e:\n                    logger.error(f\"Error stopping thread {attr_name}: {str(e)}\")\n\n        # Clean up image handler resources\n        if self.image_handler:\n            try:\n                self.image_handler.close_resources()\n            except Exception as e:\n                logger.error(f\"Error closing image handler: {str(e)}\")\n\n        # Close database connection\n        if hasattr(self, 'db_manager') and self.db_manager:\n            try:\n                self.db_manager.close()\n            except Exception as e:\n                logger.error(f\"Error closing database connection: {str(e)}\")\n\n        # Clean up temp files\n        temp_dir = tempfile.gettempdir()\n        pattern = \"trace_temp_*\"\n        try:\n            for item in os.listdir(temp_dir):\n                if item.startswith(\"trace_temp_\"):\n                    item_path = os.path.join(temp_dir, item)\n                    try:\n                        if os.path.isfile(item_path):\n                            os.remove(item_path)\n                        elif os.path.isdir(item_path):\n                            import shutil\n                            shutil.rmtree(item_path)\n                    except Exception as e:\n                        logger.error(f\"Error removing temp file {item_path}: {str(e)}\")\n        except Exception as e:\n            logger.error(f\"Error cleaning up temp files: {str(e)}\")\n\n        # Release any other resources\n        gc.collect()  # Encourage garbage collection\n\n    def load_image_evidence(self):\n        \"\"\"Open an image with a specific filter on Kali Linux.\"\"\"\n        # Define the supported image file extensions, including both lowercase and uppercase variants\n        supported_image_extensions = [\"*.e01\", \"*.E01\", \"*.s01\", \"*.S01\",\n                                      \"*.l01\", \"*.L01\", \"*.raw\", \"*.RAW\",\n                                      \"*.img\", \"*.IMG\", \"*.dd\", \"*.DD\",\n                                      \"*.iso\", \"*.ISO\", \"*.ad1\", \"*.AD1\",\n                                      \"*.001\", \"*.s01\", \"*.ex01\", \"*.dmg\",\n                                      \"*.sparse\", \"*.sparseimage\"]\n\n        # Construct the file filter string with both uppercase and lowercase extensions\n        file_filter = \"Supported Image Files ({})\".format(\" \".join(supported_image_extensions))\n\n        # Open file dialog with the specified file filter\n        image_path, _ = QFileDialog.getOpenFileName(self, \"Select Image\", \"\", file_filter)\n\n        if image_path:\n            try:\n                image_path = os.path.normpath(image_path)\n\n                # Create a progress dialog to show loading status\n                progress = QProgressDialog(\"Loading image...\", \"Cancel\", 0, 100, self)\n                progress.setWindowTitle(\"Loading Evidence\")\n                progress.setWindowModality(Qt.WindowModal)\n                progress.setMinimumDuration(PROGRESS_MIN_DURATION)  # Show dialog only if operation takes more than threshold\n                progress.setValue(10)\n\n                # Clean up any existing ImageHandler resources\n                if self.image_handler:\n                    self.image_handler.close_resources()\n\n                # Create or update the ImageHandler instance with progress updates\n                progress.setValue(20)\n\n                # Process events to update UI\n                QApplication.processEvents()\n\n                # Create a new ImageHandler with the selected image\n                self.image_handler = ImageHandler(image_path)\n                progress.setValue(50)\n\n                # Add the image to evidence files list\n                if image_path not in self.evidence_files:\n                    self.evidence_files.append(image_path)\n\n                self.current_image_path = image_path\n                progress.setValue(70)\n\n                # Pass the image handler to widgets that need it\n                self.deleted_files_widget.set_image_handler(self.image_handler)\n                self.registry_extractor_widget.image_handler = self.image_handler\n                self.metadata_viewer.image_handler = self.image_handler\n                progress.setValue(80)\n\n                # Load partitions into tree view\n                QApplication.processEvents()\n                self.load_partitions_into_tree(image_path)\n                progress.setValue(100)\n\n                # Enable all tabs since we have a valid image\n                self.enable_tabs(True)\n\n            except Exception as e:\n                QMessageBox.critical(self, \"Error Loading Image\", f\"Failed to load image: {str(e)}\")\n                # Remove the image from evidence files if it was added but failed to load\n                if image_path in self.evidence_files:\n                    self.evidence_files.remove(image_path)\n\n    def remove_image_evidence(self):\n        if not self.evidence_files:\n            QMessageBox.warning(self, \"Remove Evidence\", \"No evidence is currently loaded.\")\n            return\n\n        # Prepare the options for the dialog\n        options = self.evidence_files + [\"Remove All\"]\n        selected_option, ok = QInputDialog.getItem(self, \"Remove Evidence File\",\n                                                   \"Select an evidence file to remove or 'Remove All':\",\n                                                   options, 0, False)\n\n        if ok:\n            if selected_option == \"Remove All\":\n                # Remove all evidence files\n                self.tree_viewer.invisibleRootItem().takeChildren()  # Remove all children from the tree viewer\n                self.clear_ui()  # Clear the UI\n                QMessageBox.information(self, \"Remove Evidence\", \"All evidence files have been removed.\")\n            else:\n                # Remove the selected evidence file\n                self.evidence_files.remove(selected_option)\n                self.remove_from_tree_viewer(selected_option)\n                self.clear_ui()\n                QMessageBox.information(self, \"Remove Evidence\", f\"{selected_option} has been removed.\")\n        # clear all tabs if there are no evidence files loaded\n        if not self.evidence_files:\n            self.clear_ui()\n            # disable all tabs\n            self.enable_tabs(False)\n            # set the icon back to the original - only if verify_image_button exists\n            if hasattr(self, 'verify_image_button'):\n                self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-blue.png'))\n\n    def remove_from_tree_viewer(self, evidence_name):\n        root = self.tree_viewer.invisibleRootItem()\n        for i in range(root.childCount()):\n            item = root.child(i)\n            if item.text(0) == evidence_name:\n                root.removeChild(item)\n                break\n\n    def load_partitions_into_tree(self, image_path):\n        \"\"\"Load partitions from an image into the tree viewer.\"\"\"\n        root_item_tree = self.create_tree_item(self.tree_viewer, image_path,\n                                               self.db_manager.get_icon_path('device', 'media-optical'),\n                                               {\"start_offset\": 0})\n\n        partitions = self.image_handler.get_partitions()\n\n        # Check if the image has partitions or a recognizable file system\n        if not partitions:\n            if self.image_handler.has_filesystem(0):\n                # The image has a filesystem but no partitions, populate root directory\n                self.populate_contents(root_item_tree, {\"start_offset\": 0})\n            else:\n                # Entire image is considered as unallocated space\n                size_in_bytes = self.image_handler.get_size()\n                readable_size = self.image_handler.get_readable_size(size_in_bytes)\n                unallocated_item_text = f\"Unallocated Space: Size: {readable_size}\"\n                self.create_tree_item(root_item_tree, unallocated_item_text,\n                                      self.db_manager.get_icon_path('file', 'unknown'),\n                                      {\"is_unallocated\": True, \"start_offset\": 0,\n                                       \"end_offset\": size_in_bytes // SECTOR_SIZE})\n            return\n\n        for addr, desc, start, length in partitions:\n            end = start + length - 1\n            size_in_bytes = length * SECTOR_SIZE\n            readable_size = self.image_handler.get_readable_size(size_in_bytes)\n            fs_type = self.image_handler.get_fs_type(start)\n            desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc\n            item_text = f\"vol{addr} ({desc_str}: {start}-{end}, Size: {readable_size}, FS: {fs_type})\"\n            icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')\n            data = {\"inode_number\": None, \"start_offset\": start, \"end_offset\": end}\n            item = self.create_tree_item(root_item_tree, item_text, icon_path, data)\n\n            # Determine if the partition is special or contains unallocated space\n            special_partitions = [\"Primary Table\", \"Safety Table\", \"GPT Header\"]\n            is_special = any(special_case in desc_str for special_case in special_partitions)\n            is_unallocated = \"Unallocated\" in desc_str or \"Microsoft reserved\" in desc_str\n\n            if is_special:\n                item.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicator)\n            elif is_unallocated:\n                item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)\n                # Directly add unallocated space under the partition\n                self.create_tree_item(item, f\"Unallocated Space: Size: {readable_size}\",\n                                      self.db_manager.get_icon_path('file', 'unknown'),\n                                      {\"is_unallocated\": True, \"start_offset\": start, \"end_offset\": end})\n            else:\n                if self.image_handler.check_partition_contents(start):\n                    item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)\n                else:\n                    item.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicator)\n\n    def populate_contents(self, item: QTreeWidgetItem, data: Dict[str, Any], inode: Optional[int] = None) -> None:\n        \"\"\"Populate tree widget item with directory contents.\"\"\"\n        if self.current_image_path is None:\n            return\n\n        entries = self.image_handler.get_directory_contents(data[\"start_offset\"], inode)\n\n        for entry in entries:\n            self._create_tree_item_for_entry(item, entry, data[\"start_offset\"])\n\n    def on_item_expanded(self, item):\n        # Check if the item already has children; if so, don't repopulate\n        if item.childCount() > 0:\n            return\n\n        data = item.data(0, Qt.UserRole)\n        if data is None:\n            return\n\n        if data.get(\"inode_number\") is None:  # It's a partition\n            self.populate_contents(item, data)\n        else:  # It's a directory\n            self.populate_contents(item, data, data.get(\"inode_number\"))\n\n    class FileContentWorker(QThread):\n        \"\"\"Worker thread class for handling file operations in the background.\"\"\"\n        completed = Signal(bytes, object)\n        error = Signal(str)\n\n        def __init__(self, image_handler, inode_number, offset):\n            super().__init__()\n            self.image_handler = image_handler\n            self.inode_number = inode_number\n            self.offset = offset\n\n        def run(self):\n            try:\n                file_content, metadata = self.image_handler.get_file_content(self.inode_number, self.offset)\n                if file_content:\n                    self.completed.emit(file_content, metadata)\n                else:\n                    self.error.emit(\"Unable to read file content.\")\n            except Exception as e:\n                self.error.emit(f\"Error reading file: {str(e)}\")\n\n    # Worker thread for opening media files for streaming (doesn't load content into memory)\n    class MediaStreamWorker(QThread):\n        completed = Signal(object, int, object)  # file_obj, file_size, metadata\n        error = Signal(str)\n\n        def __init__(self, image_handler, inode_number, offset):\n            super().__init__()\n            self.image_handler = image_handler\n            self.inode_number = inode_number\n            self.offset = offset\n\n        def run(self):\n            try:\n                # Get filesystem info\n                fs = self.image_handler.get_fs_info(self.offset)\n                if not fs:\n                    self.error.emit(\"Unable to get filesystem info.\")\n                    return\n\n                # Open the file object (don't read content)\n                file_obj = fs.open_meta(inode=self.inode_number)\n                if not file_obj:\n                    self.error.emit(\"Unable to open file.\")\n                    return\n\n                file_size = file_obj.info.meta.size\n                metadata = file_obj.info.meta\n\n                if file_size == 0:\n                    self.error.emit(\"File has no content or is a special metafile!\")\n                    return\n\n                # Return the file object for streaming (don't read content)\n                self.completed.emit(file_obj, file_size, metadata)\n\n            except Exception as e:\n                self.error.emit(f\"Error opening file for streaming: {str(e)}\")\n\n    # Create a worker thread class for handling unallocated space operations in the background\n    class UnallocatedSpaceWorker(QThread):\n        completed = Signal(bytes)\n        error = Signal(str)\n\n        def __init__(self, image_handler, start_offset, end_offset):\n            super().__init__()\n            self.image_handler = image_handler\n            self.start_offset = start_offset\n            self.end_offset = end_offset\n\n        def run(self):\n            try:\n                unallocated_space = self.image_handler.read_unallocated_space(self.start_offset, self.end_offset)\n                if unallocated_space:\n                    self.completed.emit(unallocated_space)\n                else:\n                    self.error.emit(\"Unable to read unallocated space.\")\n            except Exception as e:\n                self.error.emit(f\"Error reading unallocated space: {str(e)}\")\n\n    def on_item_clicked(self, item, column):\n        self.clear_viewers()\n\n        data = item.data(0, Qt.UserRole)\n        if not data:\n            return\n\n        # Store the current selection data\n        self.current_selected_data = data\n\n        # Show a status message in the UI to indicate loading\n        statusbar = self.statusBar()\n        statusbar.showMessage(\"Loading content...\")\n\n        # Use a background worker thread if processing large files or unallocated space\n        try:\n            # Check if this is the root disk image item (has start_offset but no type/inode)\n            if (data.get(\"start_offset\") == 0 and\n                not data.get(\"type\") and\n                not data.get(\"inode_number\") and\n                not data.get(\"is_unallocated\")):\n                # This is the root disk image - display all volumes/partitions\n                self.display_volumes_in_listing()\n                statusbar.clearMessage()\n                return\n\n            if data.get(\"is_unallocated\"):\n                # Handle unallocated space in background\n                self.unallocated_worker = self.UnallocatedSpaceWorker(\n                    self.image_handler, data[\"start_offset\"], data[\"end_offset\"])\n                self.unallocated_worker.completed.connect(\n                    lambda content: self.update_viewer_with_file_content(content, data))\n                self.unallocated_worker.error.connect(\n                    lambda msg: (self.log_error(msg), statusbar.clearMessage()))\n                self.unallocated_worker.start()\n\n            elif data.get(\"type\") == \"directory\":\n                # For directories, find parent inode to enable up navigation\n                if \"parent_inode\" not in data and data.get(\"inode_number\"):\n                    parent_inode = self.find_parent_inode(data[\"start_offset\"], data[\"inode_number\"])\n                    if parent_inode:\n                        data[\"parent_inode\"] = parent_inode\n                        # Update the stored data with parent information\n                        self.current_selected_data = data\n\n                # Handle directories - populate the listing synchronously\n                entries = self.image_handler.get_directory_contents(data[\"start_offset\"], data.get(\"inode_number\"))\n\n                # Update current path for directory navigation\n                if data.get(\"name\"):\n                    if data.get(\"inode_number\") == 5:  # Root directory\n                        self.current_path = \"/\"\n                    else:\n                        # If it's a regular directory, update the path\n                        self.current_path = data.get(\"path\", os.path.join(self.current_path, data.get(\"name\", \"\")))\n\n                # Update directory up button state\n                self.update_directory_up_button()\n\n                # Populate the listing table with directory contents\n                self.populate_listing_table(entries, data[\"start_offset\"])\n\n                # Add to navigation history\n                self._add_to_history(data)\n\n                statusbar.clearMessage()\n\n            elif data.get(\"inode_number\") is not None:\n                # Handle files in background\n                self.file_worker = self.FileContentWorker(\n                    self.image_handler, data[\"inode_number\"], data[\"start_offset\"])\n                self.file_worker.completed.connect(\n                    lambda content, _: self.update_viewer_with_file_content(content, data))\n                self.file_worker.error.connect(\n                    lambda msg: (self.log_error(msg), statusbar.clearMessage()))\n                self.file_worker.start()\n\n            elif data.get(\"start_offset\") is not None:\n                # Handle partitions\n                entries = self.image_handler.get_directory_contents(data[\"start_offset\"],\n                                                                    5)  # 5 is the root inode for NTFS\n\n                # Reset path to root when viewing partitions\n                self.current_path = \"/\"\n\n                # Treat partition as a volume for history\n                if \"type\" not in data:\n                    data[\"type\"] = \"volume\"\n                if \"inode_number\" not in data:\n                    data[\"inode_number\"] = 5\n\n                self.populate_listing_table(entries, data[\"start_offset\"])\n\n                # Add to navigation history\n                self._add_to_history(data)\n\n                statusbar.clearMessage()\n\n            else:\n                self.log_error(\"Clicked item is not a file, directory, or unallocated space.\")\n                statusbar.clearMessage()\n\n        except Exception as e:\n            self.log_error(f\"Error processing item: {str(e)}\")\n            statusbar.clearMessage()\n\n    def update_directory_up_button(self):\n        \"\"\"Update the state of the directory up button based on current selection\"\"\"\n        if not self.current_selected_data:\n            self.go_up_action.setEnabled(False)\n            return\n\n        # Check if this is a directory\n        if self.current_selected_data.get(\"type\") == \"directory\":\n            inode_number = self.current_selected_data.get(\"inode_number\")\n            start_offset = self.current_selected_data.get(\"start_offset\")\n\n            # Check if this is root directory (inode 5 in NTFS)\n            is_root = inode_number == 5\n\n            # If parent_inode isn't set yet, try to find it\n            if \"parent_inode\" not in self.current_selected_data and not is_root and inode_number is not None:\n                parent_inode = self.find_parent_inode(start_offset, inode_number)\n                if parent_inode:\n                    # Update the dictionary in place\n                    self.current_selected_data[\"parent_inode\"] = parent_inode\n\n            has_parent = self.current_selected_data.get(\"parent_inode\") is not None\n            self.go_up_action.setEnabled(not is_root and has_parent)\n        else:\n            self.go_up_action.setEnabled(False)\n\n    def find_parent_inode(self, start_offset, inode_number):\n        \"\"\"Helper method to find the parent inode for a directory from tree view\"\"\"\n        try:\n            # Root directory (5 is typically root in NTFS) has no parent\n            if inode_number == 5:\n                return None\n\n            # Get directory entries for the directory\n            entries = self.image_handler.get_directory_contents(start_offset, inode_number)\n\n            # Look for parent directory entry (..)\n            for entry in entries:\n                if entry.get(\"name\") == \"..\":\n                    return entry.get(\"inode_number\")\n\n            # If we can't find the proper parent, return None\n            return None\n\n        except Exception as e:\n            self.log_error(f\"Error finding parent inode: {str(e)}\")\n            return None\n\n    def navigate_up_directory(self):\n        \"\"\"Navigate to the parent directory\"\"\"\n        if not self.current_selected_data:\n            return\n\n        # Ensure we have valid information\n        start_offset = self.current_selected_data.get(\"start_offset\")\n        inode_number = self.current_selected_data.get(\"inode_number\")\n\n        if not start_offset or not inode_number:\n            return\n\n        # If parent_inode isn't already set, try to find it\n        if \"parent_inode\" not in self.current_selected_data and self.current_selected_data.get(\"type\") == \"directory\":\n            parent_inode = self.find_parent_inode(start_offset, inode_number)\n            if parent_inode:\n                self.current_selected_data[\"parent_inode\"] = parent_inode\n\n        # Make sure we have a parent to navigate to\n        parent_inode = self.current_selected_data.get(\"parent_inode\")\n        if not parent_inode:\n            return\n\n        statusbar = self.statusBar()\n        statusbar.showMessage(\"Loading parent directory...\")\n\n        try:\n            # Create data for parent directory\n            parent_data = {\n                \"inode_number\": parent_inode,\n                \"start_offset\": start_offset,\n                \"type\": \"directory\",\n                # Get the grandparent inode if available (for consecutive up navigation)\n                \"parent_inode\": self.get_grandparent_inode(parent_inode, start_offset)\n            }\n\n            # Update current path (navigate to parent directory)\n            self.current_path = os.path.dirname(self.current_path)\n            if self.current_path == \"\":\n                self.current_path = \"/\"\n\n            # Load the parent directory\n            entries = self.image_handler.get_directory_contents(\n                parent_data[\"start_offset\"],\n                parent_data[\"inode_number\"]\n            )\n\n            self.current_selected_data = parent_data\n\n            # Update directory up button state\n            self.update_directory_up_button()\n\n            # Update both the tree view selection and listing table\n            self.populate_listing_table(entries, parent_data[\"start_offset\"])\n\n            # Add to navigation history\n            self._add_to_history(parent_data)\n\n            # Find and select the corresponding item in the tree view if possible\n            self.select_tree_item_by_inode(parent_data[\"inode_number\"], parent_data[\"start_offset\"])\n\n            statusbar.clearMessage()\n\n        except Exception as e:\n            self.log_error(f\"Error navigating to parent directory: {str(e)}\")\n            statusbar.clearMessage()\n\n    def _add_to_history(self, directory_data):\n        \"\"\"Add a directory to the navigation history.\"\"\"\n        # Skip if we're navigating through history\n        if self._navigating_history:\n            return\n\n        # Only add directories to history (not files)\n        if directory_data.get(\"type\") != \"directory\" and directory_data.get(\"type\") != \"volume\":\n            return\n\n        # Create a history entry with essential data\n        history_entry = {\n            \"inode_number\": directory_data.get(\"inode_number\"),\n            \"start_offset\": directory_data.get(\"start_offset\"),\n            \"type\": directory_data.get(\"type\"),\n            \"name\": directory_data.get(\"name\"),\n            \"path\": self.current_path,\n            \"parent_inode\": directory_data.get(\"parent_inode\")\n        }\n\n        # If we're in the middle of history (not at the end), remove everything after current position\n        if self._history_index < len(self._directory_history) - 1:\n            self._directory_history = self._directory_history[:self._history_index + 1]\n\n        # Add new entry to history\n        self._directory_history.append(history_entry)\n        self._history_index = len(self._directory_history) - 1\n\n        # Update navigation buttons\n        self._update_navigation_buttons()\n\n    def _update_navigation_buttons(self):\n        \"\"\"Update the enabled state of Back/Forward navigation buttons.\"\"\"\n        # Enable Back button if we can go back\n        can_go_back = self._history_index > 0\n        self.back_action.setEnabled(can_go_back)\n\n        # Enable Forward button if we can go forward\n        can_go_forward = self._history_index < len(self._directory_history) - 1\n        self.forward_action.setEnabled(can_go_forward)\n\n    def navigate_back(self):\n        \"\"\"Navigate to the previous directory in history.\"\"\"\n        if self._history_index <= 0:\n            return\n\n        try:\n            # Set flag to prevent adding to history\n            self._navigating_history = True\n\n            # Move back in history\n            self._history_index -= 1\n            history_entry = self._directory_history[self._history_index]\n\n            # Navigate to the directory\n            self._navigate_to_history_entry(history_entry)\n\n        finally:\n            # Always clear the flag\n            self._navigating_history = False\n            self._update_navigation_buttons()\n\n    def navigate_forward(self):\n        \"\"\"Navigate to the next directory in history.\"\"\"\n        if self._history_index >= len(self._directory_history) - 1:\n            return\n\n        try:\n            # Set flag to prevent adding to history\n            self._navigating_history = True\n\n            # Move forward in history\n            self._history_index += 1\n            history_entry = self._directory_history[self._history_index]\n\n            # Navigate to the directory\n            self._navigate_to_history_entry(history_entry)\n\n        finally:\n            # Always clear the flag\n            self._navigating_history = False\n            self._update_navigation_buttons()\n\n    def _navigate_to_history_entry(self, history_entry):\n        \"\"\"Navigate to a specific directory from history.\"\"\"\n        statusbar = self.statusBar()\n        statusbar.showMessage(\"Navigating...\")\n\n        try:\n            # Restore the path\n            self.current_path = history_entry.get(\"path\", \"/\")\n\n            # Get directory contents\n            inode_number = history_entry.get(\"inode_number\")\n            start_offset = history_entry.get(\"start_offset\")\n\n            if history_entry.get(\"type\") == \"volume\":\n                # For volumes, get root directory (inode 5)\n                entries = self.image_handler.get_directory_contents(start_offset, 5)\n            else:\n                # For regular directories, use stored inode\n                entries = self.image_handler.get_directory_contents(start_offset, inode_number)\n\n            # Update current selected data\n            self.current_selected_data = history_entry.copy()\n\n            # Update directory up button state\n            self.update_directory_up_button()\n\n            # Populate the listing table\n            self.populate_listing_table(entries, start_offset)\n\n            # Find and select the corresponding item in the tree view if possible\n            if inode_number:\n                self.select_tree_item_by_inode(inode_number, start_offset)\n\n            statusbar.clearMessage()\n\n        except Exception as e:\n            self.log_error(f\"Error navigating from history: {str(e)}\")\n            statusbar.clearMessage()\n\n    def select_tree_item_by_inode(self, inode_number, start_offset):\n        \"\"\"Attempt to find and select the item in the tree view that matches the given inode\"\"\"\n        try:\n            # Skip if inode_number is None\n            if inode_number is None:\n                return\n\n            # Get the root items\n            root_item = self.tree_viewer.invisibleRootItem()\n\n            # Find the item with matching inode and start_offset (recursive search)\n            found_item = self.find_tree_item_recursive(root_item, inode_number, start_offset)\n\n            if found_item:\n                # Temporarily disconnect the item clicked signal to prevent loops\n                self.tree_viewer.itemClicked.disconnect(self.on_item_clicked)\n\n                # Select the item and make it visible\n                self.tree_viewer.setCurrentItem(found_item)\n                self.tree_viewer.scrollToItem(found_item)\n\n                # Reconnect the signal\n                self.tree_viewer.itemClicked.connect(self.on_item_clicked)\n        except Exception as e:\n            self.log_error(f\"Error selecting tree item: {str(e)}\")\n\n    def find_tree_item_recursive(self, parent_item, inode_number, start_offset):\n        \"\"\"Recursively search for a tree item with matching inode and start_offset\"\"\"\n        # Check all children of the parent item\n        for i in range(parent_item.childCount()):\n            item = parent_item.child(i)\n            data = item.data(0, Qt.UserRole)\n\n            # Check if this item matches (allow matching based on inode only)\n            if data and data.get(\"inode_number\") == inode_number:\n                # If start_offset is also provided and doesn't match, continue searching\n                if start_offset is not None and data.get(\"start_offset\") != start_offset:\n                    continue\n                return item\n\n            # If it has children, search recursively\n            if item.childCount() > 0:\n                found = self.find_tree_item_recursive(item, inode_number, start_offset)\n                if found:\n                    return found\n\n        # Not found\n        return None\n\n    def display_volumes_in_listing(self) -> None:\n        \"\"\"Display all volumes/partitions in the listing table when disk image root is clicked.\"\"\"\n        # Clear existing content\n        self.listing_table.setRowCount(0)\n        self.listing_table.setSortingEnabled(False)\n\n        # Show columns with volume information, hide file-specific columns\n        self.listing_table.setColumnHidden(1, False)  # Show Inode (for Volume #)\n        self.listing_table.setColumnHidden(4, False)  # Show Created (for Start Offset)\n        self.listing_table.setColumnHidden(5, False)  # Show Accessed (for End Offset)\n        self.listing_table.setColumnHidden(6, False)  # Show Modified (for Length)\n        self.listing_table.setColumnHidden(7, False)  # Show Changed (for Block Size)\n        self.listing_table.setColumnHidden(8, True)   # Hide Path (not relevant for volumes)\n        self.listing_table.setColumnHidden(9, False)  # Show Info (for additional details)\n\n        # Update column headers for volume context\n        self.listing_table.setHorizontalHeaderLabels([\n            'Name', 'Volume #', 'Type', 'Size', 'Start Offset', 'End Offset',\n            'Length', 'Block Size', 'Path', 'Details'\n        ])\n\n        # Make Info column much wider for detailed information\n        self.listing_table.setColumnWidth(9, 1200)\n\n        # Reset path to root\n        self.current_path = \"/\"\n\n        # Clear navigation history when returning to disk image root\n        self._directory_history = []\n        self._history_index = -1\n        self._update_navigation_buttons()\n\n        # Disable the up button since we're at the disk image root\n        self.go_up_action.setEnabled(False)\n\n        # Get all partitions\n        partitions = self.image_handler.get_partitions()\n\n        if not partitions:\n            # No partitions found\n            self.listing_table.setSortingEnabled(True)\n            return\n\n        try:\n            for addr, desc, start, length in partitions:\n                row_position = self.listing_table.rowCount()\n                self.listing_table.insertRow(row_position)\n\n                # Calculate volume information\n                end = start + length - 1\n                size_in_bytes = length * SECTOR_SIZE\n                readable_size = self.image_handler.get_readable_size(size_in_bytes)\n                fs_type = self.image_handler.get_fs_type(start)\n                desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc\n\n                # Get additional filesystem details\n                try:\n                    fs_info = self.image_handler.get_fs_info(start)\n                    if fs_info and hasattr(fs_info.info, 'block_size'):\n                        block_size = f\"{fs_info.info.block_size:,} bytes\"\n                    else:\n                        block_size = \"N/A\"\n                except:\n                    block_size = \"N/A\"\n\n                # Volume name\n                volume_name = f\"vol{addr}\"\n                name_item = QTableWidgetItem(volume_name)\n                icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')\n                name_item.setIcon(QIcon(icon_path))\n\n                # Store volume data for potential future use\n                volume_data = {\n                    \"name\": volume_name,\n                    \"type\": \"volume\",\n                    \"start_offset\": start,\n                    \"end_offset\": end,\n                    \"addr\": addr,\n                    \"description\": desc_str,\n                    \"filesystem\": fs_type\n                }\n                name_item.setData(Qt.UserRole, volume_data)\n\n                # Create table items with detailed information\n                inode_item = QTableWidgetItem(str(addr))  # Volume number in Inode column\n                type_item = QTableWidgetItem(fs_type)\n                size_item = QTableWidgetItem(readable_size)\n\n                # Use timestamp columns for partition geometry\n                start_offset_item = QTableWidgetItem(f\"{start:,} sectors\")\n                end_offset_item = QTableWidgetItem(f\"{end:,} sectors\")\n                length_item = QTableWidgetItem(f\"{length:,} sectors\")\n                block_size_item = QTableWidgetItem(block_size)\n\n                # Build comprehensive info string\n                info_parts = []\n                # Add description first without label if it exists\n                if desc_str and desc_str.strip():\n                    info_parts.append(desc_str)\n                # Add detailed partition information\n                info_parts.append(f\"Start: {start:,} sectors ({start * SECTOR_SIZE:,} bytes)\")\n                info_parts.append(f\"End: {end:,} sectors ({end * SECTOR_SIZE:,} bytes)\")\n                info_parts.append(f\"Length: {length:,} sectors ({size_in_bytes:,} bytes)\")\n                if block_size != \"N/A\":\n                    info_parts.append(f\"Block Size: {block_size}\")\n                info_parts.append(f\"Filesystem: {fs_type}\")\n\n                info_item = QTableWidgetItem(\" | \".join(info_parts))\n\n                # Set items in table\n                self.listing_table.setItem(row_position, 0, name_item)\n                self.listing_table.setItem(row_position, 1, inode_item)\n                self.listing_table.setItem(row_position, 2, type_item)\n                self.listing_table.setItem(row_position, 3, size_item)\n                self.listing_table.setItem(row_position, 4, start_offset_item)\n                self.listing_table.setItem(row_position, 5, end_offset_item)\n                self.listing_table.setItem(row_position, 6, length_item)\n                self.listing_table.setItem(row_position, 7, block_size_item)\n                self.listing_table.setItem(row_position, 9, info_item)\n\n        finally:\n            self.listing_table.setSortingEnabled(True)\n\n    def populate_listing_table(self, entries: List[Dict[str, Any]], offset: int) -> None:\n        \"\"\"Populate the listing table with directory entries in batches for better performance.\"\"\"\n        # Clear existing content\n        self.listing_table.setRowCount(0)\n\n        # Restore original column headers for file/folder view\n        self.listing_table.setHorizontalHeaderLabels([\n            'Name', 'Inode', 'Type', 'Size', 'Created Date', 'Accessed Date',\n            'Modified Date', 'Changed Date', 'Path', 'Info'\n        ])\n\n        # Show columns relevant for files/folders, hide Info column\n        self.listing_table.setColumnHidden(1, False)  # Show Inode\n        self.listing_table.setColumnHidden(4, False)  # Show Created\n        self.listing_table.setColumnHidden(5, False)  # Show Accessed\n        self.listing_table.setColumnHidden(6, False)  # Show Modified\n        self.listing_table.setColumnHidden(7, False)  # Show Changed\n        self.listing_table.setColumnHidden(8, False)  # Show Path\n        self.listing_table.setColumnHidden(9, True)   # Hide Info\n\n        if not entries:\n            return\n\n        # Enable/disable the up button based on whether we're in the root directory\n        self.update_directory_up_button()\n\n        # Disable sorting and updates for better performance during bulk population\n        self.listing_table.setSortingEnabled(False)\n        self.listing_table.setUpdatesEnabled(False)\n\n        try:\n            total_entries = len(entries)\n\n            # Process in batches to keep UI responsive\n            for batch_start in range(0, total_entries, TABLE_BATCH_SIZE):\n                batch_end = min(batch_start + TABLE_BATCH_SIZE, total_entries)\n                batch = entries[batch_start:batch_end]\n\n                # Populate the batch\n                for entry in batch:\n                    row_position = self.listing_table.rowCount()\n                    self._populate_table_entry(row_position, entry, offset)\n\n                # Process events periodically to keep UI responsive\n                if batch_end < total_entries:\n                    QApplication.processEvents()\n\n        finally:\n            # Re-enable updates and sorting\n            self.listing_table.setUpdatesEnabled(True)\n            self.listing_table.setSortingEnabled(True)\n\n    def insert_row_into_listing_table(self, entry_name, entry_inode, description, icon_name, icon_type, offset, size,\n                                      created, accessed, modified, changed, parent_inode=None):\n        \"\"\"Insert a row into the listing table with proper caching and error handling.\"\"\"\n        try:\n            icon_path = self.db_manager.get_icon_path(icon_type, icon_name)\n            icon = QIcon(icon_path)\n            row_position = self.listing_table.rowCount() - 1  # Current row (rows are 0-indexed)\n\n            # Calculate the full path for this item\n            file_path = os.path.join(self.current_path, entry_name) if entry_name != \"..\" else os.path.dirname(\n                self.current_path)\n\n            name_item = QTableWidgetItem(entry_name)\n            name_item.setIcon(icon)\n            name_item.setData(Qt.UserRole, {\n                \"inode_number\": entry_inode,\n                \"start_offset\": offset,\n                \"type\": \"directory\" if icon_type == 'folder' else 'file',\n                \"name\": entry_name,\n                \"size\": size,\n                \"parent_inode\": parent_inode,  # Store parent directory inode for \"Go Up\" functionality\n                \"path\": file_path  # Store the full path\n            })\n\n            self.listing_table.setItem(row_position, 0, name_item)\n            self.listing_table.setItem(row_position, 1, QTableWidgetItem(str(entry_inode)))\n            self.listing_table.setItem(row_position, 2, QTableWidgetItem(description))\n            self.listing_table.setItem(row_position, 3, QTableWidgetItem(str(size)))\n            self.listing_table.setItem(row_position, 4, QTableWidgetItem(str(created)))\n            self.listing_table.setItem(row_position, 5, QTableWidgetItem(str(accessed)))\n            self.listing_table.setItem(row_position, 6, QTableWidgetItem(str(modified)))\n            self.listing_table.setItem(row_position, 7, QTableWidgetItem(str(changed)))\n            self.listing_table.setItem(row_position, 8, QTableWidgetItem(file_path))\n            self.listing_table.setItem(row_position, 9, QTableWidgetItem(\"\"))  # Empty Info column for files/folders\n\n        except Exception as e:\n            self.log_error(f\"Error adding row to listing table: {str(e)}\")\n            # Try to recover by removing the incomplete row\n            try:\n                if row_position >= 0:\n                    self.listing_table.removeRow(row_position)\n            except:\n                pass\n\n    def update_viewer_with_file_content(self, file_content, data):\n        \"\"\"Update the active viewer tab with the file content.\n\n        This method is called after file content is loaded, either directly\n        or from a background thread.\n        \"\"\"\n        # Clear the status message if it exists\n        statusbar = self.statusBar()\n        statusbar.clearMessage()\n\n        # Get the active tab index\n        index = self.viewer_tab.currentIndex()\n\n        if not file_content:\n            self.log_error(\"No content available to display\")\n            return\n\n        # Use optimized display methods for each viewer type\n        try:\n            if index == 0:  # Hex tab\n                self.hex_viewer.display_hex_content(file_content)\n            elif index == 1:  # Text tab\n                self.text_viewer.display_text_content(file_content)\n            elif index == 2:  # Application tab\n                full_file_path = data.get(\"name\", \"\")  # Retrieve the name from the data dictionary\n                self.application_viewer.display_application_content(file_content, full_file_path)\n            elif index == 3:  # File Metadata tab\n                self.metadata_viewer.display_metadata(data)\n            elif index == 4:  # Exif Data tab\n                self.exif_viewer.load_and_display_exif_data(file_content)\n            elif index == 5:  # Assuming VirusTotal tab is the 6th tab (0-based index)\n                file_hash = hashlib.md5(file_content).hexdigest()\n                self.virus_total_api.set_file_hash(file_hash)\n                self.virus_total_api.set_file_content(file_content, data.get(\"name\", \"\"))\n        except Exception as e:\n            self.log_error(f\"Error displaying content in viewer: {str(e)}\")\n\n    def update_viewer_with_media_stream(self, file_obj, file_size, metadata, data):\n        \"\"\"Update the application viewer with a media stream for playback.\"\"\"\n        # Clear the status message if it exists\n        statusbar = self.statusBar()\n        statusbar.clearMessage()\n\n        try:\n            # Determine MIME type from file extension\n            full_file_path = data.get(\"name\", \"\")\n            file_extension = os.path.splitext(full_file_path)[-1].lower()\n\n            # Map extension to MIME type\n            mime_type = None\n            if file_extension in ['.mp3', '.wav', '.ogg', '.aac', '.m4a']:\n                mime_type = f'audio/{file_extension[1:]}'\n            elif file_extension in ['.mp4', '.mkv', '.flv', '.avi', '.mov', '.webm', '.wmv', '.m4v']:\n                mime_type = 'video/mp4'\n            else:\n                mime_type = 'application/octet-stream'\n\n            # Call the load method with streaming parameters\n            self.application_viewer.load(\n                mime_type=mime_type,\n                path=full_file_path,\n                file_obj=file_obj,\n                file_size=file_size\n            )\n\n        except Exception as e:\n            self.log_error(f\"Error setting up media stream: {str(e)}\")\n\n    def display_content_for_active_tab(self):\n        \"\"\"Display content appropriate for the currently active tab.\"\"\"\n        if not self.current_selected_data:\n            return\n\n        statusbar = self.statusBar()\n        statusbar.showMessage(\"Updating view...\")\n\n        try:\n            # IMPORTANT: Cancel any running workers before starting new ones\n            # This prevents race conditions when switching between files\n            if hasattr(self, 'media_worker') and self.media_worker and self.media_worker.isRunning():\n                try:\n                    # Disconnect signals to prevent callbacks\n                    self.media_worker.completed.disconnect()\n                    self.media_worker.error.disconnect()\n                    # Request interruption (graceful)\n                    self.media_worker.requestInterruption()\n                    # Don't wait - let it finish naturally\n                except Exception as e:\n                    print(f\"Error cancelling media worker: {e}\")\n\n            if hasattr(self, 'file_worker') and self.file_worker and self.file_worker.isRunning():\n                try:\n                    # Disconnect signals to prevent callbacks\n                    self.file_worker.completed.disconnect()\n                    self.file_worker.error.disconnect()\n                    # Request interruption (graceful)\n                    self.file_worker.requestInterruption()\n                    # Don't wait - let it finish naturally\n                except Exception as e:\n                    print(f\"Error cancelling file worker: {e}\")\n\n            inode_number = self.current_selected_data.get(\"inode_number\")\n            offset = self.current_selected_data.get(\"start_offset\", self.current_offset)\n\n            if inode_number:\n                # Check if the active tab is Application tab (index 2) and file is audio/video\n                current_tab_index = self.viewer_tab.currentIndex()\n                file_name = self.current_selected_data.get(\"name\", \"\")\n                file_extension = os.path.splitext(file_name)[-1].lower()\n\n                # Media file extensions\n                media_extensions = ['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.mp4', '.mkv',\n                                  '.flv', '.avi', '.mov', '.webm', '.wmv', '.m4v']\n\n                # Use streaming for media files on Application tab\n                if current_tab_index == 2 and file_extension in media_extensions:\n                    # Use MediaStreamWorker for streaming playback (doesn't load content)\n                    self.media_worker = self.MediaStreamWorker(self.image_handler, inode_number, offset)\n                    self.media_worker.completed.connect(\n                        lambda file_obj, file_size, metadata: self.update_viewer_with_media_stream(\n                            file_obj, file_size, metadata, self.current_selected_data))\n                    self.media_worker.error.connect(\n                        lambda msg: (self.log_error(msg), statusbar.clearMessage()))\n                    self.media_worker.start()\n                else:\n                    # For non-media files or other tabs, use FileContentWorker (loads content)\n                    self.file_worker = self.FileContentWorker(self.image_handler, inode_number, offset)\n                    self.file_worker.completed.connect(\n                        lambda content, _: self.update_viewer_with_file_content(content, self.current_selected_data))\n                    self.file_worker.error.connect(\n                        lambda msg: (self.log_error(msg), statusbar.clearMessage()))\n                    self.file_worker.start()\n            else:\n                statusbar.clearMessage()\n        except Exception as e:\n            self.log_error(f\"Error updating active tab: {str(e)}\")\n            statusbar.clearMessage()\n\n    def open_listing_context_menu(self, position):\n        # Get the selected item\n        indexes = self.listing_table.selectedIndexes()\n        if indexes:\n            selected_item = self.listing_table.item(indexes[0].row(),\n                                                    0)  # Assuming the first column contains the item data\n            data = selected_item.data(Qt.UserRole)\n            menu = QMenu()\n\n            # If in search mode and item is a file, add \"Open File\" and \"Show in Directory\"\n            if self._search_mode and data.get('type') == 'file':\n                # Open File action\n                open_action = menu.addAction(\"Open File\")\n                open_action.triggered.connect(lambda: self.open_search_result_file(data))\n\n                # Show in Directory action\n                show_in_dir_action = menu.addAction(\"Show in Directory\")\n                show_in_dir_action.triggered.connect(lambda: self.show_file_in_directory(data))\n\n                # Add separator\n                menu.addSeparator()\n\n            # Add the 'Export' option for any file or folder\n            export_action = menu.addAction(\"Export\")\n            export_action.triggered.connect(lambda: self.handle_export(data, QFileDialog.getExistingDirectory(self,\n                                                                                                              \"Select Destination Directory\")))\n\n            menu.exec_(self.listing_table.viewport().mapToGlobal(position))\n\n    def handle_export(self, data, dest_dir):\n        \"\"\"Export the selected item in a background thread with progress display.\"\"\"\n        if not dest_dir:\n            return\n\n        try:\n            # Create a progress dialog\n            progress_dialog = QProgressDialog(\"Preparing to export...\", \"Cancel\", 0, 100, self)\n            progress_dialog.setWindowTitle(\"Exporting Files\")\n            progress_dialog.setWindowModality(Qt.WindowModal)\n            progress_dialog.setMinimumDuration(0)\n            progress_dialog.setValue(0)\n            progress_dialog.show()\n\n            # Create and configure the worker\n            self.export_worker = ExportWorker(\n                self.image_handler,\n                data[\"inode_number\"],\n                data[\"start_offset\"],\n                dest_dir,\n                data[\"name\"],\n                data[\"type\"] == \"directory\"\n            )\n\n            # Connect worker signals\n            self.export_worker.progress.connect(\n                lambda current, total: progress_dialog.setValue(int(current * 100 / total) if total > 0 else 0)\n            )\n            self.export_worker.status_update.connect(progress_dialog.setLabelText)\n            self.export_worker.error.connect(lambda msg: QMessageBox.warning(self, \"Export Error\", msg))\n            self.export_worker.finished.connect(progress_dialog.close)\n\n            # Connect the cancel button\n            progress_dialog.canceled.connect(self.export_worker.terminate)\n\n            # Start the worker\n            self.export_worker.start()\n\n        except Exception as e:\n            QMessageBox.critical(self, \"Export Error\", f\"Error starting export: {str(e)}\")\n\n    def log_error(self, message):\n        \"\"\"Log an error message to the console and potentially to a log file.\"\"\"\n        logger.error(f\"Error: {message}\")\n        # Could also log to a file or status bar here\n\n    def open_tree_context_menu(self, position):\n        # Get the selected item\n        indexes = self.tree_viewer.selectedIndexes()\n        if indexes:\n            selected_item = self.tree_viewer.itemFromIndex(indexes[0])\n            menu = QMenu()\n            data = selected_item.data(0, Qt.UserRole)\n\n            # Check if the selected item is a root item (disk image)\n            if selected_item and selected_item.parent() is None:\n                view_os_info_action = menu.addAction(\"View Image Information\")\n                view_os_info_action.triggered.connect(lambda: self.view_os_information(indexes[0]))\n\n            # Add the 'Export' option for any file or folder\n            export_action = menu.addAction(\"Export\")\n            export_action.triggered.connect(\n                lambda: self.handle_export(self.tree_viewer.itemFromIndex(indexes[0]).data(0, Qt.UserRole),\n                                           QFileDialog.getExistingDirectory(self, \"Select Destination Directory\")))\n\n            menu.exec_(self.tree_viewer.viewport().mapToGlobal(position))\n\n    def view_os_information(self, index):\n        \"\"\"Display comprehensive disk image information with space allocation pie chart.\"\"\"\n        item = self.tree_viewer.itemFromIndex(index)\n        if item is None or item.parent() is not None:\n            # Ensure that only the root item triggers the information display\n            return\n\n        # Create modern dialog\n        dialog = QDialog(self)\n        dialog.setWindowTitle(\"Disk Image Information\")\n        dialog.resize(1200, 800)\n\n        # Main vertical layout\n        main_layout = QVBoxLayout(dialog)\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n\n        # === TOP SECTION: Image Overview with Chart ===\n        top_widget = QWidget()\n        top_widget.setStyleSheet(\"background-color: #f8f9fa; border-bottom: 2px solid #dee2e6;\")\n        top_layout = QHBoxLayout(top_widget)\n        top_layout.setContentsMargins(20, 20, 20, 20)\n        top_layout.setSpacing(30)\n\n        # Left: Image Summary Card\n        summary_card = QWidget()\n        summary_card.setStyleSheet(\"\"\"\n            QWidget {\n                background-color: white;\n                border-radius: 8px;\n                border: 1px solid #dee2e6;\n            }\n        \"\"\")\n        summary_layout = QVBoxLayout(summary_card)\n        summary_layout.setContentsMargins(20, 20, 20, 20)\n        summary_layout.setSpacing(12)\n\n        # Title\n        title_label = QLabel(\"Disk Image Overview\")\n        title_label.setStyleSheet(\"font-size: 16pt; font-weight: bold; color: #212529; border: none;\")\n        summary_layout.addWidget(title_label)\n\n        # Key info\n        image_info = self._get_image_info()\n        key_fields = [\"Image Path\", \"Image Type\", \"Total Size\", \"Partition Scheme\", \"Number of Partitions\", \"Status\"]\n\n        for field in key_fields:\n            if field in image_info:\n                info_row = QWidget()\n                info_row.setStyleSheet(\"border: none;\")\n                info_row_layout = QHBoxLayout(info_row)\n                info_row_layout.setContentsMargins(0, 0, 0, 0)\n                info_row_layout.setSpacing(10)\n\n                label = QLabel(f\"{field}:\")\n                label.setStyleSheet(\"font-weight: bold; color: #495057; font-size: 10pt; border: none;\")\n                label.setMinimumWidth(140)\n\n                value = QLabel(str(image_info[field]))\n                value.setStyleSheet(\"color: #212529; font-size: 10pt; border: none;\")\n                value.setTextInteractionFlags(Qt.TextSelectableByMouse)\n                value.setWordWrap(True)\n\n                info_row_layout.addWidget(label)\n                info_row_layout.addWidget(value, 1)\n\n                summary_layout.addWidget(info_row)\n\n        summary_layout.addStretch()\n        summary_card.setFixedWidth(450)\n\n        # Right: Pie Chart with Legend\n        chart_widget = QWidget()\n        chart_widget.setStyleSheet(\"\"\"\n            QWidget {\n                background-color: white;\n                border-radius: 8px;\n                border: 1px solid #dee2e6;\n            }\n        \"\"\")\n        chart_outer_layout = QVBoxLayout(chart_widget)\n        chart_outer_layout.setContentsMargins(15, 15, 15, 15)\n        chart_outer_layout.setSpacing(10)\n\n        chart_title = QLabel(\"Space Allocation\")\n        chart_title.setStyleSheet(\"font-size: 14pt; font-weight: bold; color: #212529; border: none;\")\n        chart_title.setAlignment(Qt.AlignCenter)\n        chart_outer_layout.addWidget(chart_title)\n\n        # Create horizontal layout for legend (left) and chart (right)\n        chart_content_layout = QHBoxLayout()\n        chart_content_layout.setSpacing(15)\n\n        # Create chart\n        chart_view, partition_info_list = self._create_space_allocation_chart()\n\n        # Compact legend on the left\n        if partition_info_list:\n            legend_widget = QWidget()\n            legend_widget.setStyleSheet(\"border: none;\")\n            legend_layout = QVBoxLayout(legend_widget)\n            legend_layout.setContentsMargins(5, 5, 5, 5)\n            legend_layout.setSpacing(6)\n\n            for label_text, color in partition_info_list:\n                legend_row = QWidget()\n                legend_row.setStyleSheet(\"border: none;\")\n                legend_row_layout = QHBoxLayout(legend_row)\n                legend_row_layout.setContentsMargins(0, 0, 0, 0)\n                legend_row_layout.setSpacing(8)\n\n                color_indicator = QLabel()\n                color_indicator.setFixedSize(16, 16)\n                color_indicator.setStyleSheet(f\"\"\"\n                    background-color: rgb({color.red()}, {color.green()}, {color.blue()});\n                    border: 1px solid #adb5bd;\n                    border-radius: 3px;\n                \"\"\")\n\n                text_label = QLabel(label_text)\n                text_label.setStyleSheet(\"color: #495057; font-size: 9pt; border: none;\")\n                text_label.setWordWrap(True)\n\n                legend_row_layout.addWidget(color_indicator)\n                legend_row_layout.addWidget(text_label, 1)\n\n                legend_layout.addWidget(legend_row)\n\n            legend_layout.addStretch()\n            legend_widget.setMaximumWidth(300)\n            chart_content_layout.addWidget(legend_widget)\n\n        chart_content_layout.addWidget(chart_view, 1)\n        chart_outer_layout.addLayout(chart_content_layout, 1)\n\n        top_layout.addWidget(summary_card)\n        top_layout.addWidget(chart_widget, 1)\n\n        main_layout.addWidget(top_widget)\n\n        # === BOTTOM SECTION: Detailed Partition Information ===\n        bottom_widget = QWidget()\n        bottom_layout = QVBoxLayout(bottom_widget)\n        bottom_layout.setContentsMargins(20, 20, 20, 20)\n        bottom_layout.setSpacing(15)\n\n        # Section title\n        details_title = QLabel(\"Volume Details\")\n        details_title.setStyleSheet(\"font-size: 14pt; font-weight: bold; color: #212529; padding-bottom: 10px;\")\n        bottom_layout.addWidget(details_title)\n\n        # Professional table view for volume information\n        volume_table = QTableWidget()\n        volume_table.setSortingEnabled(True)\n        volume_table.verticalHeader().setVisible(False)\n        volume_table.setObjectName(\"volumeInfoTable\")\n        volume_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n        volume_table.setAlternatingRowColors(True)\n        volume_table.setEditTriggers(QTableWidget.NoEditTriggers)\n        volume_table.setIconSize(QSize(24, 24))\n        volume_table.setSelectionBehavior(QTableWidget.SelectRows)\n\n        # Enable horizontal scrolling for smaller windows\n        volume_table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)\n        volume_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n\n        # Set column count and headers\n        volume_table.setColumnCount(10)\n        volume_table.setHorizontalHeaderLabels([\n            'Volume', 'Filesystem', 'Offset (Sectors)', 'Block Size', 'Volume Size',\n            'Total Blocks', 'First Block', 'Last Block', 'Inode Count', 'Root Inode'\n        ])\n\n        # Configure header - all columns use Interactive mode for horizontal scrolling\n        header = volume_table.horizontalHeader()\n        for i in range(10):\n            header.setSectionResizeMode(i, QHeaderView.Interactive)\n\n        # Set column widths\n        volume_table.setColumnWidth(0, 100)   # Volume\n        volume_table.setColumnWidth(1, 120)   # Filesystem\n        volume_table.setColumnWidth(2, 140)   # Offset\n        volume_table.setColumnWidth(3, 100)   # Block Size\n        volume_table.setColumnWidth(4, 120)   # Volume Size\n        volume_table.setColumnWidth(5, 120)   # Total Blocks\n        volume_table.setColumnWidth(6, 120)   # First Block\n        volume_table.setColumnWidth(7, 120)   # Last Block\n        volume_table.setColumnWidth(8, 120)   # Inode Count\n        volume_table.setColumnWidth(9, 100)   # Root Inode\n\n        # Set header alignment\n        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)\n\n        # Populate table with partition data\n        partitions = self.image_handler.get_partitions()\n\n        if partitions:\n            self._populate_volume_table(volume_table, partitions)\n        else:\n            # Show message in table if no partitions\n            volume_table.setRowCount(1)\n            no_part_item = QTableWidgetItem(\"No partitions detected or single filesystem image\")\n            no_part_item.setForeground(QBrush(QColor(108, 117, 125)))\n            font = no_part_item.font()\n            font.setItalic(True)\n            no_part_item.setFont(font)\n            volume_table.setItem(0, 0, no_part_item)\n            volume_table.setSpan(0, 0, 1, 10)\n\n        bottom_layout.addWidget(volume_table, 1)\n\n        # Close button at bottom right\n        button_layout = QHBoxLayout()\n        button_layout.addStretch()\n\n        close_button = QPushButton(\"Close\")\n        close_button.clicked.connect(dialog.accept)\n        close_button.setMinimumWidth(100)\n\n        button_layout.addWidget(close_button)\n        bottom_layout.addLayout(button_layout)\n\n        main_layout.addWidget(bottom_widget, 1)\n\n        dialog.exec_()\n\n    def _populate_volume_table(self, table, partitions):\n        \"\"\"Populate the volume table with partition information.\"\"\"\n        table.setRowCount(len(partitions))\n        table.setSortingEnabled(False)  # Disable sorting while populating\n\n        for idx, partition in enumerate(partitions):\n            addr, desc, start, length = partition\n\n            # Get volume information\n            volume_info = self._extract_comprehensive_volume_info(start)\n\n            # Combine all info\n            all_info = {}\n            all_info.update(volume_info[\"basic\"])\n            all_info.update(volume_info[\"filesystem\"])\n\n            # Get filesystem type for icon\n            fs_type = all_info.get(\"Filesystem Type\", \"Unknown\")\n            icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')\n\n            # Column 0: Volume (with icon)\n            desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc\n            volume_text = f\"vol{addr}\"\n            if desc_str and desc_str.strip():\n                volume_text += f\" ({desc_str})\"\n\n            volume_item = QTableWidgetItem(volume_text)\n            volume_item.setIcon(QIcon(icon_path))\n            table.setItem(idx, 0, volume_item)\n\n            # Column 1: Filesystem\n            fs_item = QTableWidgetItem(fs_type)\n            table.setItem(idx, 1, fs_item)\n\n            # Column 2: Offset (Sectors)\n            offset_value = all_info.get(\"Partition Offset\", \"N/A\")\n            # Extract just the sector count\n            if \"sectors\" in offset_value:\n                offset_value = offset_value.split(\"sectors\")[0].strip()\n            offset_item = QTableWidgetItem(offset_value)\n            table.setItem(idx, 2, offset_item)\n\n            # Column 3: Block Size\n            block_size = all_info.get(\"Block Size\", \"N/A\")\n            block_size_item = QTableWidgetItem(block_size)\n            table.setItem(idx, 3, block_size_item)\n\n            # Column 4: Volume Size\n            volume_size = all_info.get(\"Volume Size\", \"N/A\")\n            volume_size_item = QTableWidgetItem(volume_size)\n            table.setItem(idx, 4, volume_size_item)\n\n            # Column 5: Total Blocks\n            total_blocks = all_info.get(\"Total Blocks\", \"N/A\")\n            total_blocks_item = QTableWidgetItem(total_blocks)\n            table.setItem(idx, 5, total_blocks_item)\n\n            # Column 6: First Block\n            first_block = all_info.get(\"First Block\", \"N/A\")\n            first_block_item = QTableWidgetItem(first_block)\n            table.setItem(idx, 6, first_block_item)\n\n            # Column 7: Last Block\n            last_block = all_info.get(\"Last Block\", \"N/A\")\n            last_block_item = QTableWidgetItem(last_block)\n            table.setItem(idx, 7, last_block_item)\n\n            # Column 8: Inode Count\n            inode_count = all_info.get(\"Inode Count\", \"N/A\")\n            inode_count_item = QTableWidgetItem(inode_count)\n            table.setItem(idx, 8, inode_count_item)\n\n            # Column 9: Root Inode\n            root_inode = all_info.get(\"Root Inode\", \"N/A\")\n            root_inode_item = QTableWidgetItem(root_inode)\n            table.setItem(idx, 9, root_inode_item)\n\n        table.setSortingEnabled(True)  # Re-enable sorting after populating\n\n    def _extract_comprehensive_volume_info(self, start_offset):\n        \"\"\"Extract basic pytsk3 information from a volume.\"\"\"\n        info = {\n            \"basic\": {},\n            \"filesystem\": {}\n        }\n\n        try:\n            # Get filesystem info\n            fs_info = self.image_handler.get_fs_info(start_offset)\n            if not fs_info:\n                info[\"basic\"][\"Status\"] = \"Unable to access filesystem\"\n                return info\n\n            fs_type = self.image_handler.get_fs_type(start_offset)\n\n            # === BASIC INFO ===\n            info[\"basic\"][\"Partition Offset\"] = f\"{start_offset:,} sectors ({start_offset * 512:,} bytes)\"\n            info[\"basic\"][\"Filesystem Type\"] = fs_type or \"Unknown\"\n\n            if hasattr(fs_info.info, 'block_size'):\n                info[\"basic\"][\"Block Size\"] = f\"{fs_info.info.block_size:,} bytes\"\n            if hasattr(fs_info.info, 'block_count'):\n                total_blocks = fs_info.info.block_count\n                total_size = total_blocks * fs_info.info.block_size\n                info[\"basic\"][\"Total Blocks\"] = f\"{total_blocks:,}\"\n                info[\"basic\"][\"Volume Size\"] = FileSystemUtils.get_readable_size(total_size)\n\n            # === FILESYSTEM DETAILS ===\n            if hasattr(fs_info.info, 'first_block'):\n                info[\"filesystem\"][\"First Block\"] = f\"{fs_info.info.first_block:,}\"\n            if hasattr(fs_info.info, 'last_block'):\n                info[\"filesystem\"][\"Last Block\"] = f\"{fs_info.info.last_block:,}\"\n            if hasattr(fs_info.info, 'inum_count'):\n                info[\"filesystem\"][\"Inode Count\"] = f\"{fs_info.info.inum_count:,}\"\n            if hasattr(fs_info.info, 'root_inum'):\n                info[\"filesystem\"][\"Root Inode\"] = f\"{fs_info.info.root_inum}\"\n\n        except Exception as e:\n            logger.error(f\"Error extracting volume info: {e}\")\n            info[\"basic\"][\"Error\"] = str(e)\n\n        return info\n\n    def _get_image_info(self):\n        \"\"\"Extract comprehensive disk image information.\"\"\"\n        info = {}\n\n        try:\n            # Basic image info\n            info[\"Image Path\"] = self.image_handler.image_path\n            info[\"Image Type\"] = self.image_handler.get_image_type().upper()\n\n            # Image size\n            total_size = self.image_handler.get_size()\n            info[\"Total Size\"] = FileSystemUtils.get_readable_size(total_size)\n            info[\"Total Size (Bytes)\"] = f\"{total_size:,}\"\n\n            # Sector information\n            sector_count = total_size // 512\n            info[\"Total Sectors\"] = f\"{sector_count:,}\"\n            info[\"Bytes per Sector\"] = \"512\"\n\n            # Volume information\n            if self.image_handler.volume_info:\n                try:\n                    vol_type = self.image_handler.volume_info.info.vstype\n                    volume_types = {\n                        pytsk3.TSK_VS_TYPE_DOS: \"DOS/MBR\",\n                        pytsk3.TSK_VS_TYPE_GPT: \"GPT (GUID Partition Table)\",\n                        pytsk3.TSK_VS_TYPE_MAC: \"Mac Partition Map\",\n                        pytsk3.TSK_VS_TYPE_BSD: \"BSD Disk Label\",\n                        pytsk3.TSK_VS_TYPE_SUN: \"Sun VTOC\",\n                    }\n                    info[\"Partition Scheme\"] = volume_types.get(vol_type, f\"Unknown ({vol_type})\")\n                    info[\"Number of Partitions\"] = len(self.image_handler.get_partitions())\n                except Exception as e:\n                    logger.debug(f\"Could not get volume type: {e}\")\n            else:\n                info[\"Partition Scheme\"] = \"No partition table detected\"\n\n            # Check if wiped\n            if self.image_handler.is_wiped():\n                info[\"Status\"] = \"⚠️ Wiped/Empty Image\"\n            else:\n                info[\"Status\"] = \"✓ Valid Image\"\n\n            # File modification time\n            if os.path.exists(self.image_handler.image_path):\n                mod_time = os.path.getmtime(self.image_handler.image_path)\n                info[\"File Modified\"] = datetime.datetime.fromtimestamp(mod_time).strftime(\"%Y-%m-%d %H:%M:%S\")\n\n        except Exception as e:\n            logger.error(f\"Error getting image info: {e}\")\n            info[\"Error\"] = str(e)\n\n        return info\n\n    def _get_partition_info(self, partition):\n        \"\"\"Extract detailed partition information.\"\"\"\n        info = {}\n\n        try:\n            addr, desc, start, length = partition\n\n            # Basic partition info (skip description as it's in the group box title)\n            info[\"Start Offset (Sectors)\"] = f\"{start:,}\"\n            info[\"Start Offset (Bytes)\"] = f\"{start * 512:,}\"\n            info[\"Length (Sectors)\"] = f\"{length:,}\"\n            info[\"Length (Bytes)\"] = f\"{length * 512:,}\"\n            info[\"Size\"] = FileSystemUtils.get_readable_size(length * 512)\n\n            # Filesystem information\n            try:\n                fs_info = self.image_handler.get_fs_info(start)\n                if fs_info:\n                    fs_type = self.image_handler.get_fs_type(start)\n                    info[\"File System\"] = fs_type or \"Unknown\"\n\n                    # Block/cluster information\n                    if hasattr(fs_info.info, 'block_size'):\n                        info[\"Block Size\"] = f\"{fs_info.info.block_size:,} bytes\"\n                    if hasattr(fs_info.info, 'block_count'):\n                        info[\"Block Count\"] = f\"{fs_info.info.block_count:,}\"\n\n                    # First and last block\n                    if hasattr(fs_info.info, 'first_block'):\n                        info[\"First Block\"] = f\"{fs_info.info.first_block:,}\"\n                    if hasattr(fs_info.info, 'last_block'):\n                        info[\"Last Block\"] = f\"{fs_info.info.last_block:,}\"\n\n                    # Inode information\n                    if hasattr(fs_info.info, 'inum_count'):\n                        info[\"Inode Count\"] = f\"{fs_info.info.inum_count:,}\"\n                    if hasattr(fs_info.info, 'root_inum'):\n                        info[\"Root Inode\"] = f\"{fs_info.info.root_inum}\"\n\n                    # OS detection for NTFS\n                    if fs_type == \"NTFS\":\n                        os_version = self.image_handler.get_windows_version(start)\n                        if os_version:\n                            info[\"Operating System\"] = os_version\n\n                    # Try to get volume label\n                    try:\n                        root_dir = fs_info.open_dir(path=\"/\")\n                        for entry in root_dir:\n                            if hasattr(entry, 'info') and hasattr(entry.info, 'name'):\n                                name = entry.info.name.name.decode('utf-8', errors='ignore')\n                                if name in [\"$VOLUME\", \"volume\", \".volume\"]:\n                                    # Found volume label\n                                    break\n                    except:\n                        pass\n\n                else:\n                    info[\"File System\"] = \"Could not open filesystem\"\n\n            except Exception as e:\n                info[\"File System\"] = f\"Error: {str(e)}\"\n                logger.debug(f\"Error getting filesystem info for partition: {e}\")\n\n        except Exception as e:\n            logger.error(f\"Error getting partition info: {e}\")\n            info[\"Error\"] = str(e)\n\n        return info\n\n    def _get_filesystem_colors(self):\n        \"\"\"Return consistent color mapping for filesystem types.\"\"\"\n        return {\n            \"NTFS\": QColor(41, 128, 185),      # Blue\n            \"FAT32\": QColor(46, 204, 113),     # Green\n            \"FAT16\": QColor(26, 188, 156),     # Turquoise\n            \"FAT12\": QColor(22, 160, 133),     # Dark Turquoise\n            \"exFAT\": QColor(52, 152, 219),     # Light Blue\n            \"EXT4\": QColor(231, 76, 60),       # Red\n            \"EXT3\": QColor(192, 57, 43),       # Dark Red\n            \"EXT2\": QColor(155, 89, 182),      # Purple\n            \"HFS+\": QColor(241, 196, 15),      # Yellow\n            \"APFS\": QColor(243, 156, 18),      # Orange\n            \"ISO9660\": QColor(230, 126, 34),   # Dark Orange\n            \"Unallocated\": QColor(149, 165, 166),  # Gray\n            \"Unknown\": QColor(127, 140, 141),  # Dark Gray\n        }\n\n    def _create_space_allocation_chart(self):\n        \"\"\"Create a pie chart showing allocated vs unallocated space.\"\"\"\n        # Create pie series\n        series = QPieSeries()\n        legend_items = []  # Track items for legend\n\n        try:\n            total_size = self.image_handler.get_size()\n            partitions = self.image_handler.get_partitions()\n\n            # Get filesystem color mapping\n            fs_colors = self._get_filesystem_colors()\n\n            # Calculate allocated space (partitions)\n            allocated_space = 0\n            partition_details = []\n\n            if partitions:\n                for part in partitions:\n                    addr, desc, start, length = part\n                    size = length * 512\n                    allocated_space += size\n\n                    # Get filesystem type\n                    fs_type = self.image_handler.get_fs_type(start)\n                    if not fs_type:\n                        fs_type = \"Unknown\"\n\n                    partition_details.append((fs_type, size, part[0]))\n\n            # Calculate unallocated space\n            unallocated_space = total_size - allocated_space\n\n            # Add partition slices with consistent colors\n            for idx, (fs_type, size, part_num) in enumerate(partition_details):\n                percentage = (size / total_size) * 100\n\n                # Don't show label on slice - use legend instead\n                slice = series.append(\"\", size)\n\n                # Use consistent color based on filesystem type\n                color = fs_colors.get(fs_type, fs_colors[\"Unknown\"])\n                slice.setColor(color)\n                slice.setLabelVisible(False)  # Hide labels on pie\n\n                # Add border between slices for clear separation\n                slice.setBorderColor(QColor(255, 255, 255))\n                slice.setBorderWidth(3)\n\n                # Add to legend with full details\n                legend_label = f\"{fs_type} - Partition {part_num} ({FileSystemUtils.get_readable_size(size)}, {percentage:.1f}%)\"\n                legend_items.append((legend_label, color))\n\n            # Add unallocated space\n            if unallocated_space > 0:\n                percentage = (unallocated_space / total_size) * 100\n                unalloc_slice = series.append(\"\", unallocated_space)\n                unalloc_slice.setColor(fs_colors[\"Unallocated\"])\n                unalloc_slice.setLabelVisible(False)\n                unalloc_slice.setBorderColor(QColor(255, 255, 255))\n                unalloc_slice.setBorderWidth(3)\n\n                # Add to legend\n                legend_label = f\"Unallocated Space ({FileSystemUtils.get_readable_size(unallocated_space)}, {percentage:.1f}%)\"\n                legend_items.append((legend_label, fs_colors[\"Unallocated\"]))\n\n            # If no partitions, show entire disk as unallocated\n            if not partitions:\n                slice = series.append(\"\", total_size)\n                slice.setColor(fs_colors[\"Unallocated\"])\n                slice.setLabelVisible(False)\n\n                # Add to legend\n                legend_label = f\"Entire Disk ({FileSystemUtils.get_readable_size(total_size)}, 100%)\"\n                legend_items.append((legend_label, fs_colors[\"Unallocated\"]))\n\n        except Exception as e:\n            logger.error(f\"Error creating allocation chart: {e}\")\n            # Add error slice\n            series.append(\"Error Loading Data\", 1)\n\n        # Create chart\n        chart = QChart()\n        chart.addSeries(series)\n        chart.setTitle(\"\")\n        chart.setAnimationOptions(QChart.SeriesAnimations)\n        chart.legend().setVisible(False)  # Use custom legend instead\n\n        # Minimal margins for maximum chart size\n        chart.setMargins(QMargins(0, 0, 0, 0))\n        chart.setBackgroundVisible(False)\n\n        # Create chart view\n        chart_view = QChartView(chart)\n        chart_view.setRenderHint(QPainter.Antialiasing)\n        chart_view.setMinimumSize(350, 350)\n        chart_view.setStyleSheet(\"border: none; background: transparent;\")\n\n        return chart_view, legend_items\n\n    def create_action(self, icon_path, text, callback):\n        action = QAction(QIcon(icon_path), text, self)\n        action.triggered.connect(callback)\n        return action\n\n    def get_grandparent_inode(self, parent_inode, start_offset):\n        \"\"\"Helper method to determine the grandparent inode\"\"\"\n        # Root directory (5 is typically root in NTFS) has no parent\n        if parent_inode == 5:\n            return None\n\n        try:\n            # Get directory entries for parent\n            parent_entries = self.image_handler.get_directory_contents(start_offset, parent_inode)\n\n            # Look for parent directory entry (..)\n            for entry in parent_entries:\n                if entry.get(\"name\") == \"..\":\n                    return entry.get(\"inode_number\")\n\n            # If we can't find the proper parent, try filesystem-specific approach\n            # For NTFS, parent of non-root directories is often inode 5\n            return 5\n\n        except Exception as e:\n            logger.error(f\"Error finding grandparent inode: {str(e)}\")\n            return None\n\n    # ==================== SEARCH AND FILTER HANDLERS ====================\n\n    def on_listing_table_item_clicked(self, item):\n        \"\"\"Handle clicks on listing table items - navigate tree view in search mode.\"\"\"\n        # Only handle navigation if we're in search mode\n        if not self._search_mode:\n            return\n\n        # Get the file data from the clicked item\n        row = item.row()\n        name_item = self.listing_table.item(row, 0)  # Name column\n        if not name_item:\n            return\n\n        file_data = name_item.data(Qt.UserRole)\n        if not file_data:\n            return\n\n        # Get the path from the file data\n        file_path = file_data.get('path', '')\n        if not file_path:\n            return\n\n        # Navigate the tree view to show this file's location\n        self.navigate_tree_to_path(file_path, file_data)\n\n    def navigate_tree_to_path(self, path, file_data):\n        \"\"\"Navigate and expand the tree view to show the specified path.\"\"\"\n        if not path or not self.tree_viewer:\n            return\n\n        # Split the path into components (e.g., \"/folder1/folder2/file.txt\" -> [\"folder1\", \"folder2\", \"file.txt\"])\n        # Remove leading/trailing slashes and split\n        path_parts = [p for p in path.split('/') if p]\n\n        if not path_parts:\n            return\n\n        # Start from the root - find the partition/volume first\n        root = self.tree_viewer.invisibleRootItem()\n        current_item = None\n\n        # Find the correct partition by matching the start_offset from file_data\n        start_offset = file_data.get('start_offset')\n        if start_offset is not None:\n            for i in range(root.childCount()):\n                child = root.child(i)\n                child_data = child.data(0, Qt.UserRole)\n                if child_data and child_data.get('start_offset') == start_offset:\n                    current_item = child\n                    current_item.setExpanded(True)\n                    break\n\n        if not current_item:\n            return\n\n        # Now traverse the path, expanding each folder\n        for part_index, part_name in enumerate(path_parts):\n            found = False\n\n            # Expand current item to load its children\n            if not current_item.isExpanded():\n                current_item.setExpanded(True)\n                # Give Qt time to process the expansion and load children\n                QApplication.processEvents()\n\n            # Search through children for the next part\n            for i in range(current_item.childCount()):\n                child = current_item.child(i)\n                child_text = child.text(0)\n\n                if child_text == part_name:\n                    current_item = child\n                    found = True\n\n                    # If this is not the last part, expand it\n                    if part_index < len(path_parts) - 1:\n                        current_item.setExpanded(True)\n                        QApplication.processEvents()\n                    break\n\n            if not found:\n                # Path component not found, stop navigation\n                break\n\n        # Select and highlight the final item\n        if current_item:\n            self.tree_viewer.setCurrentItem(current_item)\n            self.tree_viewer.scrollToItem(current_item)\n\n            # Set a special background color to highlight the search result\n            # Store the original background to restore later\n            if not hasattr(self, '_original_tree_item_background'):\n                self._original_tree_item_background = None\n\n            # Clear previous highlight\n            if hasattr(self, '_highlighted_tree_item') and self._highlighted_tree_item:\n                if self._original_tree_item_background:\n                    self._highlighted_tree_item.setBackground(0, self._original_tree_item_background)\n\n            # Save current item and its background\n            self._highlighted_tree_item = current_item\n            self._original_tree_item_background = current_item.background(0)\n\n            # Set red highlight for the found item\n            from PySide6.QtGui import QBrush, QColor\n            current_item.setBackground(0, QBrush(QColor(255, 100, 100, 100)))  # Semi-transparent red\n\n    def on_listing_search_text_changed(self):\n        \"\"\"Handle text changes in search bar - auto-clear results if empty.\"\"\"\n        search_query = self.listing_search_bar.text().strip()\n\n        # Auto-clear results when user manually empties the search bar\n        if not search_query and self._search_mode:\n            self.switch_to_browse_mode()\n\n    def trigger_listing_search(self):\n        \"\"\"Trigger search when Enter is pressed.\"\"\"\n        search_query = self.listing_search_bar.text().strip()\n\n        if not search_query:\n            # If empty, just return to browse mode\n            self.switch_to_browse_mode()\n            return\n\n        # Store the query\n        self._search_query = search_query\n\n        # Switch to search mode if not already\n        if not self._search_mode:\n            self.switch_to_search_mode()\n\n        # Perform the search\n        self.perform_search(search_query)\n\n    def _execute_search(self):\n        \"\"\"Execute the search after debounce delay.\"\"\"\n        if self._search_query:\n            # Switch to search mode and perform search\n            self.switch_to_search_mode()\n\n    def clear_listing_search(self):\n        \"\"\"Clear the search bar and return to browse mode.\"\"\"\n        self.listing_search_bar.clear()  # This will trigger on_listing_search_text_changed\n        # Return to browse mode\n        if self._search_mode:\n            self.switch_to_browse_mode()\n\n    def switch_to_search_mode(self):\n        \"\"\"Switch from Browse mode to Search mode.\"\"\"\n        if self._search_mode:\n            return  # Already in search mode\n\n        # Save current browse state\n        self._last_browsed_state = {\n            'offset': self.current_offset,\n            'path': self.current_path,\n            'directory_data': self.current_selected_data\n        }\n\n        # Switch to search mode\n        self._search_mode = True\n\n        # Keep tree view enabled - user can still navigate while searching\n        # (removed: self.tree_viewer.setEnabled(False))\n\n        # Show Path column (critical for search results)\n        self.listing_table.setColumnHidden(8, False)  # Path column\n\n        # Update status bar\n        statusbar = self.statusBar()\n        statusbar.showMessage(f\"Searching for '{self._search_query}'...\")\n\n        # Perform the search\n        self.perform_search(self._search_query)\n\n    def switch_to_browse_mode(self):\n        \"\"\"Switch from Search mode to Browse mode.\"\"\"\n        if not self._search_mode:\n            return  # Already in browse mode\n\n        # Switch to browse mode\n        self._search_mode = False\n        self._search_query = \"\"\n\n        # Clear any tree view highlights from search results\n        if hasattr(self, '_highlighted_tree_item') and self._highlighted_tree_item:\n            if hasattr(self, '_original_tree_item_background') and self._original_tree_item_background:\n                self._highlighted_tree_item.setBackground(0, self._original_tree_item_background)\n            self._highlighted_tree_item = None\n            self._original_tree_item_background = None\n\n        # Tree view stays enabled (removed: self.tree_viewer.setEnabled(True))\n\n        # Hide Path column in browse mode (tree shows location)\n        self.listing_table.setColumnHidden(8, True)\n\n        # Restore previous browse state\n        if self._last_browsed_state:\n            directory_data = self._last_browsed_state.get('directory_data')\n            path = self._last_browsed_state.get('path')\n\n            if directory_data and path:\n                try:\n                    # Navigate the tree view back to this location\n                    # This will also update the listing table via on_item_clicked\n                    self._restore_tree_selection(path, directory_data)\n                except Exception as e:\n                    self.statusBar().showMessage(f\"Error restoring directory view: {str(e)}\")\n\n        # Clear status bar\n        self.statusBar().clearMessage()\n\n    def _restore_tree_selection(self, path, directory_data):\n        \"\"\"Restore tree view selection to a previous location.\"\"\"\n        if not path or not self.tree_viewer:\n            return\n\n        # Reuse the navigate_tree_to_path logic but without the red highlight\n        path_parts = [p for p in path.split('/') if p]\n        if not path_parts:\n            # Root path, select the partition\n            root = self.tree_viewer.invisibleRootItem()\n            start_offset = directory_data.get('start_offset')\n            if start_offset is not None:\n                for i in range(root.childCount()):\n                    child = root.child(i)\n                    child_data = child.data(0, Qt.UserRole)\n                    if child_data and child_data.get('start_offset') == start_offset:\n                        self.tree_viewer.setCurrentItem(child)\n                        self.tree_viewer.scrollToItem(child)\n                        # Manually trigger the item clicked event to update the listing table\n                        self.on_item_clicked(child, 0)\n                        break\n            return\n\n        # Full path restoration\n        root = self.tree_viewer.invisibleRootItem()\n        current_item = None\n\n        # Find the correct partition\n        start_offset = directory_data.get('start_offset')\n        if start_offset is not None:\n            for i in range(root.childCount()):\n                child = root.child(i)\n                child_data = child.data(0, Qt.UserRole)\n                if child_data and child_data.get('start_offset') == start_offset:\n                    current_item = child\n                    current_item.setExpanded(True)\n                    break\n\n        if not current_item:\n            return\n\n        # Traverse the path\n        for part_index, part_name in enumerate(path_parts):\n            found = False\n            if not current_item.isExpanded():\n                current_item.setExpanded(True)\n                QApplication.processEvents()\n\n            for i in range(current_item.childCount()):\n                child = current_item.child(i)\n                if child.text(0) == part_name:\n                    current_item = child\n                    found = True\n                    if part_index < len(path_parts) - 1:\n                        current_item.setExpanded(True)\n                        QApplication.processEvents()\n                    break\n\n            if not found:\n                break\n\n        # Select the final item and trigger the click to update listing table\n        if current_item:\n            self.tree_viewer.setCurrentItem(current_item)\n            self.tree_viewer.scrollToItem(current_item)\n            # Manually trigger the item clicked event to update the listing table\n            self.on_item_clicked(current_item, 0)\n\n    def _wildcard_to_regex(self, pattern):\n        \"\"\"Convert wildcard pattern (*.pdf, name.*) to regex pattern.\"\"\"\n        # Escape special regex characters except * and ?\n        pattern = re.escape(pattern)\n        # Replace escaped wildcards with regex equivalents\n        pattern = pattern.replace(r'\\*', '.*')  # * matches any characters\n        pattern = pattern.replace(r'\\?', '.')   # ? matches single character\n        return f\"^{pattern}$\"  # Match entire string\n\n    def _matches_wildcard(self, filename, pattern):\n        \"\"\"Check if filename matches wildcard pattern.\"\"\"\n        regex_pattern = self._wildcard_to_regex(pattern)\n        return re.match(regex_pattern, filename, re.IGNORECASE) is not None\n\n    def perform_search(self, search_query):\n        \"\"\"Execute file search with wildcard support.\"\"\"\n        if not self.image_handler:\n            return\n\n        statusbar = self.statusBar()\n        statusbar.showMessage(f\"Searching for '{search_query}'...\")\n\n        try:\n            # Check if search query contains wildcards\n            has_wildcards = '*' in search_query or '?' in search_query\n\n            if has_wildcards:\n                # For wildcard searches, get all files and filter locally\n                files = self.image_handler.search_files(None)\n                # Filter by wildcard pattern\n                files = [f for f in files if self._matches_wildcard(f['name'], search_query)]\n            else:\n                # Regular substring search\n                files = self.image_handler.search_files(search_query)\n\n            # Clear and populate table\n            self.listing_table.setRowCount(0)\n            self.listing_table.setSortingEnabled(False)\n\n            # Show columns relevant for search results\n            self.listing_table.setColumnHidden(1, False)  # Show Inode\n            self.listing_table.setColumnHidden(2, False)  # Show Type (can be files or folders)\n            self.listing_table.setColumnHidden(4, False)  # Show Created\n            self.listing_table.setColumnHidden(5, False)  # Show Accessed\n            self.listing_table.setColumnHidden(6, False)  # Show Modified\n            self.listing_table.setColumnHidden(7, False)  # Show Changed\n            self.listing_table.setColumnHidden(8, False)  # Show Path (critical for search)\n            self.listing_table.setColumnHidden(9, True)   # Hide Info\n\n            # Populate with search results\n            for file in files:\n                self.insert_search_result_row(file)\n\n            self.listing_table.setSortingEnabled(True)\n\n            # Update status bar with result count\n            statusbar.showMessage(f\"{len(files)} result(s) for '{search_query}'\")\n\n        except Exception as e:\n            statusbar.showMessage(f\"Search error: {str(e)}\")\n\n    def insert_search_result_row(self, file_data):\n        \"\"\"Insert a search result into the listing table.\"\"\"\n        row_position = self.listing_table.rowCount()\n        self.listing_table.insertRow(row_position)\n\n        # Get file icon based on type\n        file_name = file_data.get('name', '')\n        is_directory = file_data.get('is_directory', False)\n\n        if is_directory:\n            # Directory icon\n            icon_path = self.db_manager.get_icon_path('folder', 'folder')\n        else:\n            # File icon based on extension\n            extension = os.path.splitext(file_name)[1].lower()\n            # Remove the dot from extension for icon lookup (e.g., '.pdf' -> 'pdf')\n            ext_without_dot = extension[1:] if extension else 'txt'\n            icon_path = self.db_manager.get_icon_path('file', ext_without_dot)\n\n        # Create name item with icon\n        name_item = QTableWidgetItem(file_name)\n        name_item.setIcon(QIcon(icon_path))\n        name_item.setData(Qt.UserRole, file_data)\n\n        # Create other items\n        inode_item = QTableWidgetItem(str(file_data.get('inode_number', '')))\n        type_item = QTableWidgetItem(\"Folder\" if is_directory else \"File\")\n        size_item = SizeTableWidgetItem(self.image_handler.get_readable_size(file_data.get('size', 0)))\n        size_item.setData(Qt.UserRole, file_data.get('size', 0))\n\n        created_item = QTableWidgetItem(file_data.get('created', ''))\n        accessed_item = QTableWidgetItem(file_data.get('accessed', ''))\n        modified_item = QTableWidgetItem(file_data.get('modified', ''))\n        changed_item = QTableWidgetItem(file_data.get('changed', ''))\n        path_item = QTableWidgetItem(file_data.get('path', ''))\n\n        # Set items in table\n        self.listing_table.setItem(row_position, 0, name_item)\n        self.listing_table.setItem(row_position, 1, inode_item)\n        self.listing_table.setItem(row_position, 2, type_item)  # Type column\n        self.listing_table.setItem(row_position, 3, size_item)\n        self.listing_table.setItem(row_position, 4, created_item)\n        self.listing_table.setItem(row_position, 5, accessed_item)\n        self.listing_table.setItem(row_position, 6, modified_item)\n        self.listing_table.setItem(row_position, 7, changed_item)\n        self.listing_table.setItem(row_position, 8, path_item)\n\n    def apply_browse_filter(self, extensions):\n        \"\"\"Apply file type filter to current directory in browse mode.\"\"\"\n        if not self.image_handler or self.current_offset is None:\n            return\n\n        try:\n            statusbar = self.statusBar()\n            statusbar.showMessage(\"Applying filter...\")\n\n            if extensions is None:\n                # No filter - show all files in current directory (need to get current inode)\n                # For simplicity, refresh the current view\n                # This requires tracking current inode - for now, we'll just clear the message\n                statusbar.showMessage(\"Show all files in current directory\")\n                # TODO: Implement proper directory refresh\n            else:\n                # Get all files from current directory and filter by extension\n                # This requires getting the current inode and filtering results\n                # For now, we'll use the list_files method from ImageHandler\n                files = self.image_handler.list_files(extensions)\n\n                # Clear and populate table with filtered results\n                self.listing_table.setRowCount(0)\n                self.listing_table.setSortingEnabled(False)\n\n                for file in files:\n                    self.insert_search_result_row(file)\n\n                self.listing_table.setSortingEnabled(True)\n                statusbar.showMessage(f\"{len(files)} file(s) matching selected types\")\n\n        except Exception as e:\n            logger.error(f\"Filter error: {str(e)}\")\n            self.statusBar().showMessage(f\"Filter error: {str(e)}\")\n\n    def open_search_result_file(self, file_data):\n        \"\"\"Open a file from search results in the viewer tabs.\"\"\"\n        # This is the same as double-clicking - open in viewer\n        # Use the existing file opening logic\n        self.load_file_content(file_data)\n\n    def show_file_in_directory(self, file_data):\n        \"\"\"Navigate to the file's directory in browse mode and select the file.\"\"\"\n        try:\n            # Clear search and switch to browse mode\n            self.listing_search_bar.clear()  # This triggers switch_to_browse_mode\n\n            # Get file's location details\n            start_offset = file_data.get('start_offset')\n            file_path = file_data.get('path', '')\n            file_inode = file_data.get('inode_number')\n\n            if start_offset is None or not file_path:\n                self.statusBar().showMessage(\"Cannot determine file location\")\n                return\n\n            # Parse the path to get parent directory\n            # file_path format: \"/path/to/file.txt\"\n            path_parts = file_path.split('/')\n            if len(path_parts) < 2:\n                # File is in root\n                parent_inode = 5\n                self.current_path = \"/\"\n            else:\n                # Need to navigate to parent directory\n                # For simplicity, navigate to root for now\n                # TODO: Implement proper path-to-inode resolution for deep directories\n                parent_inode = 5\n                self.current_path = \"/\"\n\n            # Load the parent directory contents\n            entries = self.image_handler.get_directory_contents(start_offset, parent_inode)\n            self.current_offset = start_offset\n            self.populate_listing_table(entries, start_offset)\n\n            # Find and select the file in the table\n            for row in range(self.listing_table.rowCount()):\n                item = self.listing_table.item(row, 0)\n                if item:\n                    item_data = item.data(Qt.UserRole)\n                    if item_data and item_data.get('inode_number') == file_inode:\n                        # Select this row\n                        self.listing_table.selectRow(row)\n                        # Scroll to make it visible\n                        self.listing_table.scrollToItem(item)\n                        break\n\n            # Update status bar\n            self.statusBar().showMessage(f\"Showing {file_data.get('name', 'file')} in directory\")\n\n            # TODO: Expand tree view to show this location\n            # This would require traversing the tree to find and expand the correct nodes\n\n        except Exception as e:\n            logger.error(f\"Error showing file in directory: {str(e)}\")\n            self.statusBar().showMessage(f\"Error navigating to file location: {str(e)}\")\n\n    # ==================== END SEARCH AND FILTER HANDLERS ====================\n\n    def on_listing_table_item_clicked(self, item):\n        \"\"\"Handle click events on the listing table.\"\"\"\n        row = item.row()\n\n        # Get data from the name column (column 0)\n        data = self.listing_table.item(row, 0).data(Qt.UserRole)\n        if not data:\n            return\n\n        self.current_selected_data = data\n\n        statusbar = self.statusBar()\n        statusbar.showMessage(\"Loading content...\")\n\n        try:\n            if data.get(\"type\") == \"volume\":\n                # Handle volume/partition - navigate into its root directory\n                start_offset = data.get(\"start_offset\", 0)\n\n                # Reset path to root of this volume\n                self.current_path = \"/\"\n\n                # Get root directory contents of the volume (inode 5 is typically root for NTFS)\n                entries = self.image_handler.get_directory_contents(start_offset, 5)\n\n                # Update directory up button - should be disabled since we're at volume root\n                self.update_directory_up_button()\n\n                # Populate listing table with volume contents\n                self.populate_listing_table(entries, start_offset)\n\n                # Add to navigation history\n                self._add_to_history(data)\n\n                statusbar.clearMessage()\n\n            elif data.get(\"type\") == \"directory\":\n                inode_number = data.get(\"inode_number\", 0)\n\n                # Find and select the corresponding item in the tree view if possible\n                self.select_tree_item_by_inode(inode_number, data[\"start_offset\"])\n\n                # Update current path for directory navigation\n                if data.get(\"name\") == \"..\":\n                    # Go to parent directory\n                    self.current_path = os.path.dirname(self.current_path)\n                    if self.current_path == \"\":\n                        self.current_path = \"/\"\n                elif data.get(\"inode_number\") == 5:  # Root directory\n                    self.current_path = \"/\"\n                else:\n                    # Navigate into directory\n                    self.current_path = os.path.join(self.current_path, data.get(\"name\", \"\"))\n\n                # Directories are processed synchronously\n                entries = self.image_handler.get_directory_contents(data[\"start_offset\"], inode_number)\n\n                # Update directory up button state\n                self.update_directory_up_button()\n\n                self.populate_listing_table(entries, data[\"start_offset\"])\n\n                # Add to navigation history\n                self._add_to_history(data)\n\n                statusbar.clearMessage()\n            else:\n                # Find and select the corresponding file in the tree view if possible\n                self.select_tree_item_by_inode(data.get(\"inode_number\"), data[\"start_offset\"])\n\n                # Files are processed in a background thread\n                inode_number = data.get(\"inode_number\", 0)\n                self.file_worker = self.FileContentWorker(self.image_handler, inode_number, data[\"start_offset\"])\n                self.file_worker.completed.connect(\n                    lambda content, _: self.update_viewer_with_file_content(content, data))\n                self.file_worker.error.connect(\n                    lambda msg: (self.log_error(msg), statusbar.clearMessage()))\n                self.file_worker.start()\n\n        except Exception as e:\n            self.log_error(f\"Error processing listing table click: {str(e)}\")\n            statusbar.clearMessage()\n\n\n\n# Add a worker thread for exporting files and directories\nclass ExportWorker(QThread):\n    progress = Signal(int, int)  # current, total\n    finished = Signal()\n    error = Signal(str)\n    status_update = Signal(str)\n\n    def __init__(self, image_handler, inode_number, offset, dest_dir, name, is_directory):\n        super().__init__()\n        self.image_handler = image_handler\n        self.inode_number = inode_number\n        self.offset = offset\n        self.dest_dir = dest_dir\n        self.name = name\n        self.is_directory = is_directory\n        self.total_items = 0\n        self.processed_items = 0\n\n    def run(self):\n        try:\n            if self.dest_dir:\n                if self.is_directory:\n                    self._export_directory(self.inode_number, self.offset, self.dest_dir, self.name)\n                else:\n                    self._export_file(self.inode_number, self.offset, self.dest_dir, self.name)\n            self.finished.emit()\n        except Exception as e:\n            self.error.emit(f\"Export error: {str(e)}\")\n\n    def _export_directory(self, inode_number, offset, dest_dir, name):\n        \"\"\"Export a directory with progress reporting.\"\"\"\n        try:\n            # Create the directory in the destination\n            new_dest_dir = os.path.join(dest_dir, name)\n            os.makedirs(new_dest_dir, exist_ok=True)\n\n            # Get directory contents\n            entries = self.image_handler.get_directory_contents(offset, inode_number)\n\n            # Count total items for progress reporting\n            self._count_items_recursive(entries, offset)\n\n            # Export each entry\n            for entry in entries:\n                try:\n                    self._export_item(\n                        entry[\"inode_number\"],\n                        offset,\n                        new_dest_dir,\n                        entry[\"name\"],\n                        entry[\"is_directory\"]\n                    )\n                except Exception as e:\n                    self.error.emit(f\"Error exporting {entry['name']}: {str(e)}\")\n\n        except Exception as e:\n            self.error.emit(f\"Error exporting directory {name}: {str(e)}\")\n\n    def _count_items_recursive(self, entries, offset):\n        \"\"\"Count total items in a directory and subdirectories.\"\"\"\n        self.total_items += len(entries)\n\n        # Count items in subdirectories\n        for entry in entries:\n            if entry[\"is_directory\"]:\n                sub_entries = self.image_handler.get_directory_contents(offset, entry[\"inode_number\"])\n                self._count_items_recursive(sub_entries, offset)\n\n    def _export_item(self, inode_number, offset, dest_dir, name, is_directory):\n        \"\"\"Export a single item (file or directory).\"\"\"\n        self.status_update.emit(f\"Exporting {name}\")\n\n        if is_directory:\n            sub_dest_dir = os.path.join(dest_dir, name)\n            os.makedirs(sub_dest_dir, exist_ok=True)\n\n            # Get subdirectory contents\n            entries = self.image_handler.get_directory_contents(offset, inode_number)\n\n            # Export each entry in the subdirectory\n            for entry in entries:\n                self._export_item(\n                    entry[\"inode_number\"],\n                    offset,\n                    sub_dest_dir,\n                    entry[\"name\"],\n                    entry[\"is_directory\"]\n                )\n        else:\n            self._export_file(inode_number, offset, dest_dir, name)\n\n        # Update progress\n        self.processed_items += 1\n        self.progress.emit(self.processed_items, self.total_items)\n\n    def _export_file(self, inode_number, offset, dest_dir, name):\n        \"\"\"Export a single file with chunked processing.\"\"\"\n        try:\n            file_content, _ = self.image_handler.get_file_content(inode_number, offset)\n            if file_content:\n                file_path = os.path.join(dest_dir, name)\n                with open(file_path, 'wb') as f:\n                    f.write(file_content)\n                self.processed_items += 1\n                self.progress.emit(self.processed_items, self.total_items)\n        except Exception as e:\n            self.error.emit(f\"Error exporting file {name}: {str(e)}\")\n"
  },
  {
    "path": "modules/metadata_tab.py",
    "content": "import os\nimport datetime\nfrom PySide6.QtWidgets import QTextEdit, QSizePolicy, QWidget, QVBoxLayout\nimport hashlib\nfrom magic import Magic\nimport re\n\n\nclass MetadataViewer(QWidget):\n    def __init__(self, image_handler):\n        super(MetadataViewer, self).__init__()\n        self.image_handler = image_handler\n        self.init_ui()\n\n    def init_ui(self):\n        # Add the text edit to the layout\n        layout = QVBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n        layout.setSpacing(0)\n\n        self.metadata_text_edit = QTextEdit()\n        self.metadata_text_edit.setReadOnly(True)\n        self.metadata_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n        layout.addWidget(self.metadata_text_edit)\n\n    def display_metadata(self, data):\n        # Check if this is a carved file with content already provided\n        is_carved = data.get('is_carved', False)\n        file_content = data.get('file_content')\n\n        if is_carved and file_content:\n            # Carved file - use provided content, no filesystem metadata available\n            metadata = None\n        else:\n            # Regular file - read from filesystem\n            inode_number = data.get('inode_number')\n            offset = data.get('start_offset')\n            file_content, metadata = self.image_handler.get_file_content(inode_number, offset)\n\n            if metadata is None:\n                self.metadata_text_edit.setHtml(\"<b>No metadata available.</b>\")\n                return\n\n        def format_time(timestamp):\n            if timestamp is None or timestamp == 0:\n                return \"N/A\"\n            try:\n                return datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + \" UTC\"\n            except Exception:\n                return \"N/A\"\n\n        # Handle timestamps - use filesystem metadata if available, otherwise use carved timestamp\n        if is_carved:\n            # For carved files, use the extracted/preserved timestamp\n            carved_timestamp = data.get('carved_timestamp', 'N/A')\n            created_time = 'N/A (carved file)'\n            modified_time = carved_timestamp if carved_timestamp != 'N/A' else 'N/A (carved file)'\n            accessed_time = 'N/A (carved file)'\n            changed_time = 'N/A (carved file)'\n        else:\n            # For regular files, use filesystem metadata\n            created_time = format_time(metadata.crtime) if hasattr(metadata, 'crtime') else 'N/A'\n            modified_time = format_time(metadata.mtime) if hasattr(metadata, 'mtime') else 'N/A'\n            accessed_time = format_time(metadata.atime) if hasattr(metadata, 'atime') else 'N/A'\n            changed_time = format_time(metadata.ctime) if hasattr(metadata, 'ctime') else 'N/A'\n\n        md5_hash = hashlib.md5(file_content).hexdigest() if file_content else \"N/A\"\n        sha256_hash = hashlib.sha256(file_content).hexdigest() if file_content else \"N/A\"\n        mime_type = Magic().from_buffer(file_content) if file_content else \"N/A\"\n\n        # Ensure size is an integer before passing to get_readable_size\n        if is_carved:\n            # For carved files, use size from data dict\n            size = data.get('size', 0)\n            size = self.image_handler.get_readable_size(size)\n        else:\n            # For regular files, use filesystem metadata\n            size = metadata.size if metadata.size else 'N/A'\n            if isinstance(size, str):\n                try:\n                    size = int(size)  # Convert size to int if it's a string\n                except ValueError:\n                    size = 'N/A'  # Keep as 'N/A' if conversion fails\n            else:\n                size = self.image_handler.get_readable_size(size)  # Convert size to a readable format\n\n        # extended_metadata = f\"<b>Metadata</b>\"\n        extended_metadata = f\"<b style='font-size: 20px; font-family: Courier New;'>Metadata</b>\"\n\n        # Add carved file indicator if applicable\n        if is_carved:\n            extended_metadata += f\"<p style='margin-left: 10px; font-family: Courier New; color: #ff6600;'><b>⚠ Carved File</b> (recovered from unallocated space)</p>\"\n\n        extended_metadata += f\"<table style='margin-left: 10px; font-family: Courier New;'>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Name:</th><td style='padding-left: 20px;'>{data.get('name', 'N/A')}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Type:</th><td style='padding-left: 20px;'>{data.get('type')}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>MIME Type:</th><td style='padding-left: 20px;'>{mime_type}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Size:</th><td style='padding-left: 20px;'>{size}</td></tr>\"\n\n        # Add disk offset for carved files\n        if is_carved:\n            offset_value = data.get('offset', 0)\n            extended_metadata += f\"<tr><th style='text-align: left;'>Disk Offset:</th><td style='padding-left: 20px;'>{hex(offset_value)} ({offset_value} bytes)</td></tr>\"\n\n        extended_metadata += f\"<tr><th style='text-align: left;'>Modified:</th><td style='padding-left: 20px;'>{modified_time}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Accessed:</th><td style='padding-left: 20px;'>{accessed_time}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Created:</th><td style='padding-left: 20px;'>{created_time}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>Changed:</th><td style='padding-left: 20px;'>{changed_time}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>MD5:</th><td style='padding-left: 20px;'>{md5_hash}</td></tr>\"\n        extended_metadata += f\"<tr><th style='text-align: left;'>SHA-256:</th><td style='padding-left: 20px;'>{sha256_hash}</td></tr>\"\n        extended_metadata += f\"</table>\"\n        extended_metadata += f\"<br>\"\n        extended_metadata += f\"<br>\"\n\n        # Skip istat for carved files (no inode available)\n        if not is_carved and os.name == 'nt':\n            istat_output = self.run_istat(data.get('start_offset'), data.get('inode_number'), self.image_handler.image_path)\n            extended_metadata += (\n                f\"<b style='font-size: 20px; font-family: Courier New;'>From The Sleuth Kit istat Tool</b>\")\n            extended_metadata += (f\"<div style='margin-left: 15px; font-family: Courier New;'>\")\n            extended_metadata += (f\"<pre>{istat_output}</pre>\")\n            extended_metadata += (f\"</div>\")\n\n        self.metadata_text_edit.setHtml(extended_metadata)\n\n    def run_istat(self, offset, inode_number, image_path):\n        import subprocess\n\n        if image_path is None:\n            raise ValueError(\"Image path value is None!\")\n        if inode_number is None:\n            raise ValueError(\"Inode number value is None!\")\n\n        metadata_cmd = [\"tools/sleuthkit-4.12.1-win32/bin/istat.exe\"]\n\n        if offset is not None:\n            metadata_cmd.extend([\"-o\", str(offset)])\n\n        metadata_cmd.extend([image_path, str(inode_number)])\n\n        metadata_result = subprocess.run(\n            metadata_cmd,\n            capture_output=True,\n            text=True,\n            check=True\n        )\n\n        metadata_content = metadata_result.stdout\n\n        match = re.search(r\"(init_size: \\d+)\", metadata_content)\n\n        if match:\n            end_index = match.end()\n            metadata_content = metadata_content[:end_index]\n\n        return metadata_content\n\n    def clear(self):\n        self.metadata_text_edit.clear()\n"
  },
  {
    "path": "modules/registry.py",
    "content": "import os\nimport tempfile\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QIcon\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QTextEdit, QToolBar, QLabel, \\\n    QSplitter, QTableWidget, QTableWidgetItem, QComboBox, QSizePolicy, QPushButton, QMenu, QApplication, QHeaderView\nfrom Registry import Registry\nfrom Registry.Registry import RegistryValue, RegistryKey\n\n\n\nclass RegistryExtractor(QWidget):\n    def __init__(self, image_handler):\n        super().__init__()\n        self.image_handler = image_handler\n        self.hive_icon = QIcon(\"Icons/icons8-hive-48.png\")\n        self.key_icon = QIcon(\"Icons/icons8-key-48_blue.png\")\n        self.value_icon = QIcon(\"Icons/icons8-wasp-48.png\")\n        self.init_ui()\n\n    def init_ui(self):\n        main_layout = QVBoxLayout()\n        main_layout.setContentsMargins(0, 0, 0, 0)\n        main_layout.setSpacing(0)\n        self.setLayout(main_layout)\n\n        self.toolbar = QToolBar(\"Toolbar\")\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        main_layout.addWidget(self.toolbar)\n\n        self.icon_label = QLabel()\n        self.icon_label.setPixmap(QIcon(\"Icons/icons8-registry-editor-96.png\").pixmap(48, 48))\n        self.toolbar.addWidget(self.icon_label)\n\n        self.label = QLabel(\"Registry Browser\")\n        self.label.setStyleSheet(\"\"\"\n            QLabel {\n                font-size: 20px; /* Slightly larger size for the title */\n                color: #37c6d0; /* Hex color for the text */\n                font-weight: bold; /* Make the text bold */\n                margin-left: 8px; /* Space between icon and label */\n            }\n        \"\"\")\n        self.toolbar.addWidget(self.label)\n\n        spacer = QLabel()\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(spacer)\n\n        self.hiveSelector = QComboBox()\n        self.hiveSelector.addItems([\"SOFTWARE\", \"SYSTEM\", \"SAM\", \"SECURITY\", \"DEFAULT\", \"COMPONENTS\"])\n        self.toolbar.addWidget(self.hiveSelector)\n\n        self.loadHiveButton = QPushButton(\"Load\")\n        self.loadHiveButton.clicked.connect(self.load_selected_hive)\n        self.toolbar.addWidget(self.loadHiveButton)\n\n        # Splitter setup\n        self.splitter = QSplitter(Qt.Horizontal)\n        main_layout.addWidget(self.splitter)\n\n        # Tree Widget Setup\n        self.treeWidget = QTreeWidget()\n        self.treeWidget.header().hide()\n        self.splitter.addWidget(self.treeWidget)\n\n        # Details Panel and Table Setup\n        self.detailsSplitter = QSplitter(Qt.Vertical)\n        self.splitter.addWidget(self.detailsSplitter)\n\n        # Metadata Panel Setup\n        self.metadataPanel = QTextEdit()\n        self.metadataPanel.setReadOnly(True)\n        self.detailsSplitter.addWidget(self.metadataPanel)\n\n        # Table Setup for displaying values\n        self.tableWidget = QTableWidget()\n        self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)\n        self.tableWidget.setSelectionBehavior(QTableWidget.SelectRows)\n        self.tableWidget.verticalHeader().setVisible(False)\n        self.detailsSplitter.addWidget(self.tableWidget)\n\n        # Adjust proportions\n        self.splitter.setSizes([300, 700])  # Allocate space for the tree and details\n        self.detailsSplitter.setStretchFactor(0, 1)  # Metadata panel\n        self.detailsSplitter.setStretchFactor(1, 1)  # Table panel\n\n        # Connect the click event\n        self.treeWidget.itemClicked.connect(self.on_item_clicked)\n\n    def onCustomContextMenuRequested(self, position):\n        # Create the context menu\n        contextMenu = QMenu(self)\n        copyAction = contextMenu.addAction(\"Copy\")\n\n        # Execute the menu and check which action was triggered\n        action = contextMenu.exec_(self.tableWidget.mapToGlobal(position))\n\n        if action == copyAction:\n            # Copy the selected cell's text to the clipboard\n            selectedIndexes = self.tableWidget.selectedIndexes()\n            if selectedIndexes:\n                selectedText = selectedIndexes[0].data()  # Assuming single selection for simplicity\n                QApplication.clipboard().setText(selectedText)\n\n    def load_selected_hive(self):\n        try:\n            selectedHive = self.hiveSelector.currentText()\n\n            # Assuming get_partitions returns partitions where Windows is installed\n            partitions = self.image_handler.get_partitions()\n\n            if not partitions:\n                print(\"No partitions found.\")\n                return\n\n            for partition in partitions:\n                start_offset = partition[2]\n                fs_type = self.image_handler.get_fs_type(start_offset)\n                fs_info = self.image_handler.get_fs_info(start_offset)\n                if fs_type == \"NTFS\":\n                    # Modify to only load the selected hive\n                    hive_data = self.image_handler.get_registry_hive(fs_info,\n                                                                     f\"/Windows/System32/config/{selectedHive}\")\n                    if hive_data:\n                        # Temporarily save the hive data to a file and load it\n                        with tempfile.NamedTemporaryFile(delete=False) as temp_hive:\n                            temp_hive.write(hive_data)\n                            temp_hive_path = temp_hive.name\n\n                        # Load the hive\n                        with open(temp_hive_path, \"rb\") as hive_file:\n                            reg = Registry.Registry(hive_file)\n                            self.display_registry_hive(selectedHive, reg.root())  # Display the selected hive\n\n                        os.remove(temp_hive_path)\n        except Exception as e:\n            print(f\"An error occurred while loading the selected hive: {e}\")\n\n    def display_registry_hive(self, hive_name, root_key):\n        self.treeWidget.clear()  # Clear the tree before displaying a new hive\n        hive_item = QTreeWidgetItem(self.treeWidget, [hive_name])\n        hive_item.setIcon(0, self.hive_icon)\n        hive_item.setData(0, Qt.UserRole, root_key)\n        self.display_registry_keys(hive_item, root_key)\n\n    def display_registry_keys(self, parent_item, registry_key):\n        subkeys = registry_key.subkeys()  # Call the method once and store the result\n        items = [QTreeWidgetItem(parent_item, [subkey.name()]) for subkey in subkeys]  # Use list comprehension\n        for item, subkey in zip(items, subkeys):\n            item.setData(0, Qt.UserRole, subkey)  # Store the key object for later retrieval\n            item.setIcon(0, self.key_icon)\n            self.display_registry_keys(item, subkey)\n            self.display_registry_values(item, subkey)\n\n    def display_registry_values(self, parent_key_item, registry_key):\n        values = registry_key.values()  # Call the method once and store the result\n        items = [QTreeWidgetItem(parent_key_item, [value.name() or \"(Default)\"]) for value in\n                 values]  # Use list comprehension\n        for item, value in zip(items, values):\n            item.setData(0, Qt.UserRole, value)  # Store the value object for later retrieval\n            item.setIcon(0, self.value_icon)\n\n    def display_metadata(self, registry_object):\n        metadata = {\n            \"Name\": registry_object.name(),\n            \"Number of Subkeys\": len(registry_object.subkeys()),\n            \"Number of Values\": len(registry_object.values()),\n            \"Last Modified\": registry_object.timestamp().strftime(\"%Y-%m-%d %H:%M:%S\"),\n        }\n\n        # Start with an HTML structure for styling\n        details = '<html><head/><body>'\n        details += '<p style=\"font-size:14px; font-family: Courier New; \"><b>Metadata Information</b></p>'\n\n        for key, value in metadata.items():\n            details += f'<p style=\"margin-left: 10px; font-size: 12px; font-family: Courier New;\"><b>{key}:</b> {value}</p>'\n\n        details += '</body></html>'\n\n        self.metadataPanel.setHtml(details)\n\n    def setup_table(self, values):\n        # Reset and set up table\n        self.tableWidget.clear()\n        self.tableWidget.setRowCount(len(values))\n        self.tableWidget.setColumnCount(3)\n        self.tableWidget.setHorizontalHeaderLabels([\"Name\", \"Type\", \"Value\"])\n\n        # Set initial widths to balance out based on common sizes\n        self.tableWidget.setColumnWidth(0, 150)  # Name\n        self.tableWidget.setColumnWidth(1, 150)  # Type\n        self.tableWidget.setColumnWidth(2, 450)  # Value\n\n        # Set dynamic resizing behavior\n        header = self.tableWidget.horizontalHeader()\n        header.setSectionResizeMode(0, QHeaderView.Stretch)  # Name column to stretch based on content\n        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)  # Type column adjusts to fit the content\n        header.setSectionResizeMode(2, QHeaderView.Stretch)  # Value column stretches with window resize\n\n        # Populate table rows\n        for i, value in enumerate(values):\n            self.tableWidget.setItem(i, 0, QTableWidgetItem(value.name()))\n            self.tableWidget.setItem(i, 1, QTableWidgetItem(str(value.value_type_str())))\n            self.tableWidget.setItem(i, 2, QTableWidgetItem(str(value.value())))\n\n    def display_values_in_table(self, values):\n        self.setup_table(values)\n\n    def on_item_clicked(self, item, column):\n        registry_object = item.data(0, Qt.UserRole)\n\n        if isinstance(registry_object, RegistryKey):\n            self.display_metadata(registry_object)\n            self.display_values_in_table(registry_object.values())\n\n        elif isinstance(registry_object, RegistryValue):\n            self.setup_table([registry_object])\n\n    # clear the window\n    def clear(self):\n        self.treeWidget.clear()\n        self.metadataPanel.clear()\n        self.tableWidget.clear()\n"
  },
  {
    "path": "modules/text_tab.py",
    "content": "import base64\nimport html\nimport re\nimport sqlite3\nimport urllib\nimport urllib.parse\nfrom enum import Enum\nfrom functools import partial\n\nimport chardet\nfrom PySide6.QtCore import Qt, QSize\nfrom PySide6.QtGui import QAction, QIcon, QTextCursor, QTextCharFormat, QColor\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QToolBar, QLineEdit, QSizePolicy, QComboBox, QLabel, \\\n    QMessageBox, QToolTip, QToolButton, QMenu\n\n\nclass SearchDirection(Enum):\n    NEXT = 1\n    PREVIOUS = 2\n\n\nclass TextViewerManager:\n    PAGE_SIZE = 2000\n\n    def __init__(self):\n        self.file_content = b\"\"  # Store raw byte content\n        self.text_content = \"\"  # This will store the extracted strings\n        self.current_page = 0\n        self.encoding = 'utf-8'\n        self.last_search_str = \"\"\n        self.current_match_index = -1\n        self.page_changed_callback = None\n\n        self.matches = []\n\n    def get_total_pages(self):\n        return (len(self.text_content) - 1) // self.PAGE_SIZE + 1\n\n    @staticmethod\n    def detect_encoding(file_content_chunk):\n        detector = chardet.detect(file_content_chunk)\n        encoding = detector.get('encoding')\n        return encoding if encoding else 'utf-8'\n\n    def extract_strings_from_content(self):\n        encoding = self.detect_encoding(self.file_content[:1024])  # Detect encoding based on the first 1024 bytes\n        try:\n            text = self.file_content.decode(encoding)\n        except UnicodeDecodeError:\n            text = self.file_content.decode('ISO-8859-1')  # Fallback to ISO-8859-1 if decoding fails\n\n        # Use regex to extract sequences of printable characters (length >= 4)\n        strings = re.findall(r\"[ -~]{4,}\", text)\n\n        # Join the strings with newlines to store them\n        self.text_content = \"\\n\".join(strings)\n\n    def load_text_content(self, file_content):\n        self.file_content = file_content\n        self.extract_strings_from_content()  # Extract printable strings\n        self.current_page = 0\n\n    def get_text_content_for_current_page(self):\n        start_idx = self.current_page * self.PAGE_SIZE\n        end_idx = (self.current_page + 1) * self.PAGE_SIZE\n        return self.text_content[start_idx:end_idx]\n\n    def change_page(self, delta):\n        new_page = self.current_page + delta\n        if 0 <= new_page * self.PAGE_SIZE < len(self.text_content):\n            self.current_page = new_page\n            if self.page_changed_callback:\n                self.page_changed_callback()\n\n    def jump_to_start(self):\n        self.current_page = 0\n        if self.page_changed_callback:\n            self.page_changed_callback()\n\n    def jump_to_end(self):\n        self.current_page = len(self.text_content) // self.PAGE_SIZE\n        if self.page_changed_callback:\n            self.page_changed_callback()\n\n    def search_for_string(self, search_str, direction=SearchDirection.NEXT):\n        if not search_str:  # If search string is empty, do nothing\n            return\n\n        # Only find all occurrences of the search string if the search string has changed\n        if self.last_search_str != search_str:\n            self.matches = []\n            self.current_match_index = -1\n\n            # Find all occurrences of the search string in the text content\n            start_idx = 0\n            while start_idx < len(self.text_content):\n                idx = self.text_content.find(search_str, start_idx)\n                if idx == -1:\n                    break\n                self.matches.append(idx)\n                start_idx = idx + len(search_str)  # Update the start index to the end of the current match\n\n            self.last_search_str = search_str\n\n        # If no matches were found, return\n        if not self.matches:\n            return\n\n        # Update the current match index based on the search direction\n        if direction == SearchDirection.NEXT:\n            self.current_match_index = (self.current_match_index + 1) % len(self.matches)\n        else:\n            self.current_match_index = (self.current_match_index - 1) % len(self.matches)\n\n        # Update the current page to the page containing the current match\n        match_position = self.matches[self.current_match_index]\n        self.current_page = match_position // self.PAGE_SIZE\n\n    def clear_content(self):\n        self.file_content = b\"\"\n        self.current_page = 0\n        self.last_search_str = \"\"\n        self.current_match_index = -1\n        self.matches = []\n\n\nclass TextViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.manager = TextViewerManager()\n        self.manager.page_changed_callback = self.refresh_content\n\n        self.init_ui()\n\n    def init_ui(self):\n        self.layout = QVBoxLayout(self)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n\n        self.setup_toolbar()\n        self.setup_text_edit()\n\n        self.setLayout(self.layout)\n\n    def setup_toolbar(self):\n        self.toolbar = QToolBar(self)\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.toolbar.setMovable(False)\n        self.toolbar.setIconSize(QSize(16, 16))  # Reduce icon size\n        self.toolbar.setFixedHeight(32)  # Reduce toolbar height\n        self.toolbar.setStyleSheet(\"\"\"\n            QToolBar {\n                spacing: 2px;\n                padding: 1px;\n            }\n            QToolButton {\n                padding: 2px;\n                margin: 1px;\n            }\n        \"\"\")\n        # disable right click\n        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)\n\n        # Navigation buttons\n        self.first_action = QAction(QIcon(\"Icons/icons8-thick-arrow-pointing-up-50.png\"), \"Jump to Start\", self)\n        self.first_action.triggered.connect(self.manager.jump_to_start)\n        self.toolbar.addAction(self.first_action)\n\n        self.prev_action = QAction(QIcon(\"Icons/icons8-left-arrow-50.png\"), \"Previous Page\", self)\n        self.prev_action.triggered.connect(lambda: self.manager.change_page(-1))\n        self.toolbar.addAction(self.prev_action)\n\n        # Page entry\n        self.page_entry = QLineEdit(self)\n        self.page_entry.setMaximumWidth(40)\n        self.page_entry.setFixedHeight(25)  # Set fixed height for input\n        self.page_entry.setPlaceholderText(\"1\")\n        self.page_entry.returnPressed.connect(self.go_to_page_by_entry)\n        self.toolbar.addWidget(self.page_entry)\n\n        # Total pages label\n        self.total_pages_label = QLabel(\" of \")\n        self.total_pages_label.setFixedHeight(25)  # Set fixed height for label\n        self.toolbar.addWidget(self.total_pages_label)\n\n        self.next_action = QAction(QIcon(\"Icons/icons8-right-arrow-50.png\"), \"Next Page\", self)\n        self.next_action.triggered.connect(lambda: self.manager.change_page(1))\n        self.toolbar.addAction(self.next_action)\n\n        self.last_action = QAction(QIcon(\"Icons/icons8-down-50.png\"), \"Jump to End\", self)\n        self.last_action.triggered.connect(self.manager.jump_to_end)\n        self.toolbar.addAction(self.last_action)\n\n        # Add a small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        # Font size controls\n        font_label = QLabel(\"Font Size: \")\n        font_label.setFixedHeight(25)  # Set fixed height for label\n        self.toolbar.addWidget(font_label)\n\n        self.font_size_combobox = QComboBox(self)\n        self.font_size_combobox.setFixedHeight(25)  # Set fixed height for combobox\n        self.font_size_combobox.setFixedWidth(60)  # Set fixed width to show full numbers\n        self.font_size_combobox.addItems([\"8\", \"10\", \"12\", \"14\", \"16\", \"18\", \"20\", \"24\", \"28\", \"32\", \"36\"])\n        self.font_size_combobox.currentTextChanged.connect(self.update_font_size)\n        self.toolbar.addWidget(self.font_size_combobox)\n\n        # Add a spacer to push the search bar to the right\n        spacer = QWidget(self)\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(spacer)\n\n        # Search controls\n        self.search_input = QLineEdit(self)\n        self.search_input.setPlaceholderText(\"Search...\")\n        self.search_input.setMaximumWidth(180)  # Reduce width to save space\n        self.search_input.setFixedHeight(25)  # Reduce height\n        self.search_input.setContentsMargins(5, 0, 5, 0)  # Reduce margins\n        self.search_input.returnPressed.connect(self.search_next)\n        self.toolbar.addWidget(self.search_input)\n\n        self.layout.addWidget(self.toolbar)\n\n    def setup_text_edit(self):\n        self.text_edit = CustomTextEdit(self)\n        self.text_edit.setReadOnly(True)\n        self.layout.addWidget(self.text_edit)\n\n    def display_text_content(self, file_content):\n        self.manager.load_text_content(file_content)\n        self.refresh_content()\n\n    def clear_content(self):\n        self.text_edit.clear()\n        self.manager.clear_content()\n\n    def search_next(self):\n        # Call the search_for_string method with the updated match index\n        self.manager.search_for_string(self.search_input.text(), SearchDirection.NEXT)\n\n        # Update the highlighted text to highlight the current match\n        self.update_highlighted_text()\n\n    def update_highlighted_text(self):\n        if not self.manager.matches or not (0 <= self.manager.current_match_index < len(self.manager.matches)):\n            return\n\n        cursor = self.text_edit.textCursor()\n        cursor.clearSelection()\n\n        start_pos = self.manager.matches[self.manager.current_match_index] % self.manager.PAGE_SIZE\n        end_pos = start_pos + len(self.search_input.text())\n\n        highlight_format = QTextCharFormat()\n        highlight_format.setBackground(QColor(\"yellow\"))\n\n        cursor.setPosition(start_pos, QTextCursor.MoveAnchor)\n        cursor.setPosition(end_pos, QTextCursor.KeepAnchor)\n        cursor.setCharFormat(highlight_format)\n\n    def update_font_size(self):\n        selected_size = int(self.font_size_combobox.currentText())\n        current_font = self.text_edit.font()\n        current_font.setPointSize(selected_size)\n        self.text_edit.setFont(current_font)\n\n    def go_to_page_by_entry(self):\n        try:\n            page_num = int(self.page_entry.text()) - 1\n            if 0 <= page_num < self.manager.get_total_pages():\n                self.manager.current_page = page_num\n                self.refresh_content()\n            else:\n                QMessageBox.warning(self, \"Invalid Page\", \"Page number out of range.\")\n        except ValueError:\n            QMessageBox.warning(self, \"Invalid Page\", \"Please enter a valid page number.\")\n\n    def refresh_content(self):\n        text_content = self.manager.get_text_content_for_current_page()\n        self.text_edit.setPlainText(text_content)\n        current_page = self.manager.current_page + 1  # Pages start from 1\n        total_pages = self.manager.get_total_pages()\n        self.page_entry.setText(str(current_page))\n        self.total_pages_label.setText(f\" of {total_pages}\")\n\n\nclass CustomTextEdit(QTextEdit):\n    def __init__(self, *args, **kwargs):\n        super(CustomTextEdit, self).__init__(*args, **kwargs)\n        self.setMouseTracking(True)\n\n    def contextMenuEvent(self, event):\n        menu = self.createStandardContextMenu()\n        menu.addSeparator()\n\n        # Define all decoding actions\n        decoding_actions = {\n            \"Decode Base64\": self.decodeBase64,\n            \"Decode Hex\": self.decodeHex,\n            \"Decode URL\": self.decodeURL,\n            \"Decode HTML\": self.decodeHTML,\n            \"Decode Octal\": self.decodeOctal,\n            \"Decode Binary\": self.decodeBinary,\n        }\n\n        for action_text, method in decoding_actions.items():\n            action = menu.addAction(action_text)\n            action.triggered.connect(partial(method))\n\n        menu.exec(event.globalPos())\n\n    def decodeBase64(self):\n        self.decodeSelectedText('base64')\n\n    def decodeHex(self):\n        self.decodeSelectedText('hex')\n\n    def decodeURL(self):\n        self.decodeSelectedText('url')\n\n    def decodeHTML(self):\n        self.decodeSelectedText('html')\n\n    def decodeOctal(self):\n        self.decodeSelectedText('octal')\n\n    def decodeBinary(self):\n        self.decodeSelectedText('binary')\n\n    def decodeSelectedText(self, encoding_type):\n        selected_text = self.textCursor().selectedText()\n        try:\n            if encoding_type == 'base64':\n                decoded_bytes = base64.b64decode(selected_text)\n            elif encoding_type == 'hex':\n                decoded_bytes = bytes.fromhex(selected_text)\n            elif encoding_type == 'url':\n                decoded_text = urllib.parse.unquote_plus(selected_text)\n                QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)\n                return\n            elif encoding_type == 'html':\n                decoded_text = html.unescape(selected_text)\n                QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)\n                return\n            elif encoding_type == 'octal':\n                decoded_text = ''.join(chr(int(octal, 8)) for octal in selected_text.split())\n                QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)\n                return\n            elif encoding_type == 'binary':\n                decoded_text = ''.join(chr(int(i, 2)) for i in selected_text.split())\n                QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)\n                return\n\n            if encoding_type in ['base64', 'hex']:\n                decoded_text = decoded_bytes.decode('utf-8')\n                QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)\n\n\n\n        except Exception as e:\n            QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), f\"Invalid {encoding_type.upper()}\")\n\n    def getDecodedText(self, selected_text):\n        # Attempt decoding in various formats\n        decoders = [\n            self.tryDecodeBinary,\n            self.tryDecodeOctal,\n            self.tryDecodeBase64,\n            self.tryDecodeHex,\n            self.tryDecodeURL,\n            self.tryDecodeHTML\n        ]\n        for decoder in decoders:\n            decoded_text = decoder(selected_text)\n            if decoded_text:\n                return decoded_text\n        return None\n\n    def tryDecodeBase64(self, text):\n        try:\n            decoded_bytes = base64.b64decode(text, validate=True)\n            return decoded_bytes.decode('utf-8')\n        except Exception:\n            return None\n\n    def tryDecodeHex(self, text):\n        try:\n            decoded_bytes = bytes.fromhex(text)\n            return decoded_bytes.decode('utf-8')\n        except Exception:\n            return None\n\n    def tryDecodeURL(self, text):\n        try:\n            return urllib.parse.unquote_plus(text)\n        except Exception:\n            return None\n\n    def tryDecodeHTML(self, text):\n        try:\n            return html.unescape(text)\n        except Exception:\n            return None\n\n    def tryDecodeOctal(self, text):\n        try:\n            return ''.join(chr(int(octal, 8)) for octal in text.split())\n        except Exception:\n            return None\n\n    def tryDecodeBinary(self, text):\n        try:\n            decoded_text = ''.join(chr(int(i, 2)) for i in text.split())\n\n            return decoded_text\n        except Exception:\n            return None\n\n    def mouseMoveEvent(self, event):\n        super(CustomTextEdit, self).mouseMoveEvent(event)\n        # Check if there's selected text\n        selected_text = self.textCursor().selectedText()\n        if selected_text:\n            tooltip_text = self.getDecodedText(selected_text)\n            if tooltip_text:\n                QToolTip.showText(event.globalPos(), tooltip_text)\n        else:\n            QToolTip.hideText()  # Hide any existing tooltip if there's no selection\n\n"
  },
  {
    "path": "modules/unified_application_manager.py",
    "content": "import os\nfrom ctypes import cast, POINTER\nfrom weakref import WeakValueDictionary\nimport mimetypes\nimport platform\nimport time\n\nfrom PySide6.QtCore import Qt, QUrl, Slot, QSize, QTimer, QPoint, QBuffer, QByteArray, QIODevice\nfrom PySide6.QtGui import QIcon, QPixmap, QImage, QAction, QPageLayout, QPainter, QColor, QPen, QTransform\nfrom PySide6.QtMultimedia import QMediaPlayer, QAudioOutput\nfrom PySide6.QtMultimediaWidgets import QVideoWidget\nfrom PySide6.QtPrintSupport import QPrinter, QPrintDialog\nfrom PySide6.QtWidgets import (QToolBar, QMessageBox, QScrollArea, QLineEdit, QFileDialog, QApplication)\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QSlider, QLabel, QHBoxLayout, QComboBox, \\\n    QSpacerItem, QSizePolicy\n\nfrom fitz import open as fitz_open, Matrix\n\nif os.name == \"nt\":  # Windows\n    from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume\n    from comtypes import CLSCTX_ALL\n\n\nclass PyTsk3StreamDevice(QIODevice):\n    \"\"\"Custom QIODevice that streams data directly from pytsk3 file objects. \"\"\"\n\n    def __init__(self, file_obj, file_size, parent=None):\n        \"\"\"Initialize the stream device. \"\"\"\n        super().__init__(parent)\n        self.file_obj = file_obj\n        self.file_size = file_size\n        self.current_position = 0\n        self._is_closed = False  # Track if device has been closed\n\n    def size(self):\n        \"\"\"Return the total size of the media file.\"\"\"\n        return self.file_size\n\n    def isSequential(self):\n        \"\"\"Return False to indicate this device supports seeking.\"\"\"\n        return False\n\n    def seek(self, pos):\n        \"\"\"Seek to a specific position in the file.\"\"\"\n        if 0 <= pos <= self.file_size:\n            self.current_position = pos\n            # Call parent seek to update internal state\n            return super().seek(pos)\n        return False\n\n    def pos(self):\n        \"\"\"Return the current position in the file.\"\"\"\n        return self.current_position\n\n    def atEnd(self):\n        \"\"\"Return True if at end of file.\"\"\"\n        return self.current_position >= self.file_size\n\n    def readData(self, maxSize):\n        \"\"\"Read data from the pytsk3 file object.\"\"\"\n        # Safety check: Don't read if device is closed\n        if self._is_closed:\n            return b''\n\n        if self.current_position >= self.file_size:\n            return b''\n\n        # Safety check: Make sure file_obj still exists\n        if not self.file_obj:\n            return b''\n\n        try:\n            # Calculate how much to read\n            bytes_to_read = min(maxSize, self.file_size - self.current_position)\n\n            # Read from pytsk3 file object\n            data = self.file_obj.read_random(self.current_position, bytes_to_read)\n\n            # Update position\n            self.current_position += len(data)\n\n            return data\n        except Exception as e:\n            # This is expected if the device was closed while reading\n            if not self._is_closed:\n                print(f\"Error reading from pytsk3 file object: {e}\")\n            return b''\n\n    def writeData(self, data):\n        \"\"\"Write data (not supported for read-only device).\"\"\"\n        return -1\n\n    def close(self):\n        \"\"\"Close the device and mark it as closed.\"\"\"\n        self._is_closed = True\n        self.file_obj = None  # Release reference to file object\n        super().close()\n\n\nclass UnifiedViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.current_path = None\n        self.main_app = None\n        self.layout = QVBoxLayout(self)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n\n        # Check if Icons directory exists and create it if needed\n        self.ensure_icons_directory()\n\n        # Create placeholder widget to show when nothing is loaded\n        self.placeholder = QLabel(\"No content loaded\")\n        self.placeholder.setObjectName(\"placeholderLabel\")  # For stylesheet targeting\n        self.placeholder.setAlignment(Qt.AlignCenter)\n        self.layout.addWidget(self.placeholder)\n\n        # Initialize viewers as None for lazy loading\n        self._pdf_viewer = None\n        self._picture_viewer = None\n        self._audio_video_player = None\n\n        # Store media buffer for in-memory playback (keeps buffer alive during playback)\n        self._media_buffer = None\n\n        # Store stream device and file object for streaming playback from disk images\n        self._media_stream_device = None\n        self._media_file_obj = None\n\n    def ensure_icons_directory(self):\n        \"\"\"Check if Icons directory exists and create it if needed\"\"\"\n        icons_dir = \"Icons\"\n        if not os.path.exists(icons_dir):\n            try:\n                os.makedirs(icons_dir)\n                print(f\"Created missing Icons directory: {icons_dir}\")\n\n                # Create missing default icons\n                self.create_default_icon(os.path.join(icons_dir, \"play.png\"), (50, 50), (0, 255, 0))\n                self.create_default_icon(os.path.join(icons_dir, \"pause.png\"), (50, 50), (255, 165, 0))\n                self.create_default_icon(os.path.join(icons_dir, \"stop.png\"), (50, 50), (255, 0, 0))\n                self.create_default_icon(os.path.join(icons_dir, \"volume.png\"), (50, 50), (0, 0, 255))\n                self.create_default_icon(os.path.join(icons_dir, \"mute.png\"), (50, 50), (128, 128, 128))\n            except Exception as e:\n                print(f\"Error creating Icons directory: {e}\")\n\n    def create_default_icon(self, path, size, color):\n        \"\"\"Create a simple colored square icon at the specified path\"\"\"\n        try:\n            image = QImage(size[0], size[1], QImage.Format_ARGB32)\n            # Use literal transparent color instead of Qt.transparent\n            image.fill(QColor(0, 0, 0, 0))\n\n            painter = QPainter(image)\n            painter.setPen(QPen(QColor(*color)))\n            # Create a QColor with proper alpha channel\n            brush_color = QColor(*color)\n            brush_color.setAlpha(128)  # Semi-transparent\n            painter.setBrush(brush_color)\n\n            if \"play\" in path:\n                # Draw play triangle\n                points = [\n                    QPoint(10, 10),\n                    QPoint(10, 40),\n                    QPoint(40, 25)\n                ]\n                painter.drawPolygon(points)\n            elif \"pause\" in path:\n                # Draw pause symbol\n                painter.drawRect(15, 10, 8, 30)\n                painter.drawRect(27, 10, 8, 30)\n            elif \"stop\" in path:\n                # Draw stop symbol\n                painter.drawRect(15, 15, 20, 20)\n            elif \"volume\" in path:\n                # Draw volume symbol\n                painter.drawRect(10, 20, 10, 10)\n                painter.drawArc(20, 10, 20, 30, -45 * 16, 90 * 16)\n            elif \"mute\" in path:\n                # Draw mute symbol\n                painter.drawRect(10, 20, 10, 10)\n                painter.drawLine(25, 15, 35, 35)\n                painter.drawLine(35, 15, 25, 35)\n\n            painter.end()\n            image.save(path)\n        except Exception as e:\n            print(f\"Error creating default icon {path}: {e}\")\n\n\n    def get_pdf_viewer(self):\n        \"\"\"Lazy initialization of PDF viewer\"\"\"\n        if self._pdf_viewer is None:\n            self._pdf_viewer = PDFViewer(self)\n            self._pdf_viewer.setVisible(False)\n            self.layout.addWidget(self._pdf_viewer)\n        return self._pdf_viewer\n\n    def get_picture_viewer(self):\n        \"\"\"Lazy initialization of picture viewer\"\"\"\n        if self._picture_viewer is None:\n            self._picture_viewer = PictureViewer(self)\n            self._picture_viewer.setVisible(False)\n            self.layout.addWidget(self._picture_viewer)\n        return self._picture_viewer\n\n    def get_audio_video_player(self):\n        \"\"\"Lazy initialization of audio/video player\"\"\"\n        if self._audio_video_player is None:\n            self._audio_video_player = AudioVideoPlayer(self)\n            self._audio_video_player.setVisible(False)\n            self.layout.addWidget(self._audio_video_player)\n        return self._audio_video_player\n\n    def load(self, content=None, mime_type=None, path=None, file_obj=None, file_size=None):\n        \"\"\"Load content into the appropriate viewer.\"\"\"\n        # Clear any previous content\n        self.clear()\n        self.current_path = path\n\n        # Check if we have either content or file_obj\n        if not content and not file_obj:\n            self.placeholder.setVisible(True)\n            return\n\n        try:\n            # Process PDF files\n            if mime_type.startswith('application/pdf'):\n                viewer = self.get_pdf_viewer()\n                viewer.display(content)\n                viewer.setVisible(True)\n                self.placeholder.setVisible(False)\n                return True\n\n            # Process images\n            elif mime_type.startswith('image/'):\n                viewer = self.get_picture_viewer()\n                viewer.display(content)\n                viewer.setVisible(True)\n                self.placeholder.setVisible(False)\n                return True\n\n            # Process audio and video - use streaming if file_obj provided, otherwise QBuffer\n            elif mime_type.startswith(('audio/', 'video/')):\n                player = self.get_audio_video_player()\n\n                # Create a hint URL with the mime type to help the media backend\n                # identify the format correctly\n                hint_url = QUrl()\n                hint_url.setScheme(\"memory\")\n                suffix = mimetypes.guess_extension(mime_type) or '.tmp'\n                hint_url.setPath(f\"media{suffix}\")\n\n                # OPTION 1: Stream from pytsk3 file object (for large files from disk images)\n                if file_obj is not None and file_size is not None:\n                    print(f\"Using streaming playback for {file_size} byte media file\")\n\n                    # Create custom stream device\n                    self._media_stream_device = PyTsk3StreamDevice(file_obj, file_size, self)\n\n                    # Open the stream device for reading\n                    if not self._media_stream_device.open(QIODevice.ReadOnly):\n                        print(\"Failed to open stream device for reading\")\n                        self.placeholder.setText(\"Error: Could not open stream device\")\n                        self.placeholder.setVisible(True)\n                        return False\n\n                    # Keep file_obj reference alive\n                    self._media_file_obj = file_obj\n\n                    # Set the media source from stream device\n                    player.media_player.setSourceDevice(self._media_stream_device, hint_url)\n\n                # OPTION 2: Use QBuffer for in-memory playback (small files or pre-loaded content)\n                elif content is not None:\n                    # Determine if we should use QBuffer based on size\n                    file_size_mb = len(content) / (1024 * 1024)\n                    print(f\"Using in-memory playback for {file_size_mb:.2f} MB media file\")\n\n                    # Create QBuffer for in-memory playback\n                    # QBuffer needs to stay alive during playback, so we store it as instance variable\n                    self._media_buffer = QBuffer()\n\n                    # Wrap content in QByteArray and set it to the buffer\n                    byte_array = QByteArray(content)\n                    self._media_buffer.setData(byte_array)\n\n                    # Open buffer for reading\n                    if not self._media_buffer.open(QIODevice.ReadOnly):\n                        print(\"Failed to open media buffer for reading\")\n                        self.placeholder.setText(\"Error: Could not open media buffer\")\n                        self.placeholder.setVisible(True)\n                        return False\n\n                    # Set the media source from buffer\n                    player.media_player.setSourceDevice(self._media_buffer, hint_url)\n\n                else:\n                    self.placeholder.setText(\"Error: No content or stream source provided\")\n                    self.placeholder.setVisible(True)\n                    return False\n\n                player.setVisible(True)\n                self.placeholder.setVisible(False)\n\n                # For audio files, configure for audio-only mode\n                if mime_type.startswith('audio/'):\n                    try:\n                        player.set_audio_only_mode(True)\n                    except Exception as e:\n                        print(f\"Warning: Could not set audio-only mode: {e}\")\n\n                return True\n\n            # Unsupported file type\n            else:\n                self.placeholder.setText(f\"Unsupported file type: {mime_type}\")\n                self.placeholder.setVisible(True)\n                return False\n\n        except Exception as e:\n            self.placeholder.setText(f\"Error loading content: {str(e)}\")\n            self.placeholder.setVisible(True)\n            return False\n\n    def clear(self):\n        \"\"\"Clear all viewers and free up resources.\"\"\"\n        # Hide all viewers\n        if self._pdf_viewer:\n            self._pdf_viewer.clear()\n            self._pdf_viewer.setVisible(False)\n\n        if self._picture_viewer:\n            self._picture_viewer.clear()\n            self._picture_viewer.setVisible(False)\n\n        # Clean up media player\n        if self._audio_video_player:\n            try:\n                # Stop playback\n                self._audio_video_player.stop()\n            except Exception as e:\n                print(f\"Error stopping media player: {e}\")\n            self._audio_video_player.setVisible(False)\n\n        # Clean up media buffer\n        if self._media_buffer:\n            try:\n                if self._media_buffer.isOpen():\n                    self._media_buffer.close()\n                self._media_buffer = None\n            except Exception as e:\n                print(f\"Error closing media buffer: {e}\")\n\n        # Clean up stream device - with safety delay\n        if self._media_stream_device:\n            # Store reference for delayed cleanup\n            old_stream_device = self._media_stream_device\n            old_file_obj = self._media_file_obj\n\n            # Clear references immediately\n            self._media_stream_device = None\n            self._media_file_obj = None\n\n            # Close the device after a short delay to let background threads finish\n            # This is non-blocking and happens asynchronously\n            def delayed_cleanup():\n                try:\n                    if old_stream_device and old_stream_device.isOpen():\n                        old_stream_device.close()\n                except Exception as e:\n                    print(f\"Error in delayed stream device cleanup: {e}\")\n\n            # Schedule cleanup after 100ms (non-blocking)\n            QTimer.singleShot(100, delayed_cleanup)\n        else:\n            # No stream device, just clear file object\n            self._media_file_obj = None\n\n        # Show the placeholder\n        self.placeholder.setText(\"No content loaded\")\n        self.placeholder.setVisible(True)\n        self.current_path = None\n\n    def display_application_content(self, file_content, full_file_path):\n        \"\"\"Wrapper for backward compatibility - converts file extension to MIME type.\"\"\"\n        file_extension = os.path.splitext(full_file_path)[-1].lower()\n        mime_type = None\n\n        # Map common extensions to MIME types\n        if file_extension in ['.pdf']:\n            mime_type = 'application/pdf'\n        elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif']:\n            mime_type = f'image/{file_extension[1:]}'\n        elif file_extension in ['.mp3', '.wav', '.ogg', '.aac', '.m4a']:\n            mime_type = f'audio/{file_extension[1:]}'\n        elif file_extension in ['.mp4', '.mkv', '.flv', '.avi', '.mov', '.wmv']:\n            mime_type = 'video/mp4'\n        else:\n            # Default to binary data\n            mime_type = 'application/octet-stream'\n\n        # Call the new load method with the determined MIME type\n        return self.load(file_content, mime_type, full_file_path)\n\n    def closeEvent(self, event):\n        \"\"\"Handle proper cleanup when the widget is closed\"\"\"\n        # Make sure to stop any media playback\n        if self._audio_video_player:\n            try:\n                self._audio_video_player.stop()\n            except:\n                pass\n\n        # Clean up media buffer\n        if self._media_buffer:\n            try:\n                if self._media_buffer.isOpen():\n                    self._media_buffer.close()\n                self._media_buffer = None\n            except:\n                pass\n\n        # Clean up stream device (immediate, not delayed)\n        if self._media_stream_device:\n            try:\n                if self._media_stream_device.isOpen():\n                    self._media_stream_device.close()\n                self._media_stream_device = None\n            except:\n                pass\n\n        # Release file object\n        self._media_file_obj = None\n\n        # Accept the close event\n        super().closeEvent(event)\n\n    def __del__(self):\n        \"\"\"Ensure proper cleanup when the object is garbage collected\"\"\"\n        # Clean up media resources\n        try:\n            if self._media_buffer and self._media_buffer.isOpen():\n                self._media_buffer.close()\n            if self._media_stream_device and self._media_stream_device.isOpen():\n                self._media_stream_device.close()\n            self._media_file_obj = None\n        except:\n            pass  # Ignore errors during cleanup in destructor\n\n    def shutdown(self):\n        \"\"\"Properly shut down all resources, especially media players.\n        Call this method before the application exits.\"\"\"\n        try:\n            # Force close any open viewers first\n            if self._pdf_viewer:\n                self._pdf_viewer.clear()\n\n            if self._picture_viewer:\n                self._picture_viewer.clear()\n\n            # Explicit shutdown of audio/video player\n            if self._audio_video_player:\n                try:\n                    # Stop media playback and remove references\n                    self._audio_video_player.safe_stop()\n                    QApplication.processEvents()\n\n                    # Release reference\n                    player = self._audio_video_player\n                    self._audio_video_player = None\n                except Exception as e:\n                    print(f\"Error during audio/video player shutdown: {e}\")\n\n            # Clean up media buffer\n            if self._media_buffer:\n                try:\n                    if self._media_buffer.isOpen():\n                        self._media_buffer.close()\n                    self._media_buffer = None\n                except Exception as e:\n                    print(f\"Error closing media buffer during shutdown: {e}\")\n\n            # Clean up stream device\n            if self._media_stream_device:\n                try:\n                    if self._media_stream_device.isOpen():\n                        self._media_stream_device.close()\n                    self._media_stream_device = None\n                except Exception as e:\n                    print(f\"Error closing stream device during shutdown: {e}\")\n\n            # Release file object reference\n            self._media_file_obj = None\n\n            # Process any pending events\n            QApplication.processEvents()\n\n        except Exception as e:\n            print(f\"Error during UnifiedViewer shutdown: {e}\")\n\n\nclass PictureViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.original_pixmap = None  # Store the original QPixmap\n        self.original_image_bytes = None  # Store the original image bytes\n        self.initialize_ui()\n\n    def initialize_ui(self):\n        self.layout = QVBoxLayout()\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n        self.layout.setAlignment(Qt.AlignCenter)\n\n        # Create a container for the toolbar and the application viewer\n        container_widget = QWidget(self)\n\n        container_layout = QVBoxLayout()\n        container_layout.setContentsMargins(0, 0, 0, 0)  # Remove any margins\n        container_layout.setSpacing(0)  # Remove spacing between toolbar and viewer\n\n        # Create and set up the toolbar\n        self.setup_toolbar()\n\n        # Add the toolbar to the container layout\n        container_layout.addWidget(self.toolbar)\n\n        self.image_label = QLabel(self)\n        self.image_label.setContentsMargins(0, 0, 0, 0)\n        self.image_label.setAlignment(Qt.AlignCenter)\n\n        self.scroll_area = QScrollArea(self)\n        self.scroll_area.setContentsMargins(0, 0, 0, 0)\n        self.scroll_area.setWidget(self.image_label)\n        self.scroll_area.setWidgetResizable(True)\n\n        container_layout.addWidget(self.scroll_area)\n        container_widget.setLayout(container_layout)\n        self.layout.addWidget(container_widget)\n        self.setLayout(self.layout)\n\n    def setup_toolbar(self):\n        self.toolbar = QToolBar(self)\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.toolbar.setMovable(False)\n        self.toolbar.setIconSize(QSize(16, 16))  # Reduce icon size\n        self.toolbar.setFixedHeight(32)  # Reduce toolbar height\n        # Disable right click\n        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)\n\n        zoom_in_icon = QIcon(\"Icons/icons8-zoom-in-50.png\")\n        zoom_out_icon = QIcon(\"Icons/icons8-zoom-out-50.png\")\n        rotate_left_icon = QIcon(\"Icons/icons8-rotate-left-50.png\")\n        rotate_right_icon = QIcon(\"Icons/icons8-rotate-right-50.png\")\n        reset_icon = QIcon(\"Icons/icons8-no-rotation-50.png\")\n        export_icon = QIcon(\"Icons/icons8-save-as-50.png\")\n\n        zoom_in_action = QAction(zoom_in_icon, 'Zoom In', self)\n        zoom_out_action = QAction(zoom_out_icon, 'Zoom Out', self)\n        rotate_left_action = QAction(rotate_left_icon, 'Rotate Left', self)\n        rotate_right_action = QAction(rotate_right_icon, 'Rotate Right', self)\n        reset_action = QAction(reset_icon, 'Reset', self)\n        self.export_action = QAction(export_icon, 'Save Image', self)\n\n        zoom_in_action.triggered.connect(self.zoom_in)\n        zoom_out_action.triggered.connect(self.zoom_out)\n        rotate_left_action.triggered.connect(self.rotate_left)\n        rotate_right_action.triggered.connect(self.rotate_right)\n        reset_action.triggered.connect(self.reset)\n        self.export_action.triggered.connect(self.export_original_image)\n\n        # Add actions to the toolbar\n        self.toolbar.addAction(zoom_in_action)\n        self.toolbar.addAction(zoom_out_action)\n        self.toolbar.addAction(rotate_left_action)\n        self.toolbar.addAction(rotate_right_action)\n        self.toolbar.addAction(reset_action)\n        self.toolbar.addAction(self.export_action)\n\n    def display(self, content):\n        self.original_image_bytes = content  # Save the original image bytes\n        # Convert byte data to QPixmap\n        qt_image = QImage.fromData(content)\n        pixmap = QPixmap.fromImage(qt_image)\n        self.original_pixmap = pixmap.copy()  # Save the original pixmap\n        self.image_label.setPixmap(pixmap)\n\n    def clear(self):\n        self.image_label.clear()\n\n    def zoom_in(self):\n        self.image_label.setPixmap(self.image_label.pixmap().scaled(\n            self.image_label.width() * 1.2, self.image_label.height() * 1.2, Qt.KeepAspectRatio,\n            Qt.SmoothTransformation))\n\n    def zoom_out(self):\n        self.image_label.setPixmap(self.image_label.pixmap().scaled(\n            self.image_label.width() * 0.8, self.image_label.height() * 0.8, Qt.KeepAspectRatio,\n            Qt.SmoothTransformation))\n\n    def rotate_left(self):\n        transform = QTransform().rotate(-90)\n        pixmap = self.image_label.pixmap().transformed(transform)\n        self.image_label.setPixmap(pixmap)\n\n    def rotate_right(self):\n        transform = QTransform().rotate(90)\n        pixmap = self.image_label.pixmap().transformed(transform)\n        self.image_label.setPixmap(pixmap)\n\n    def reset(self):\n        if self.original_pixmap:\n            self.image_label.setPixmap(self.original_pixmap)\n\n    def export_original_image(self):\n        # Ensure that an image is currently loaded\n        if not self.original_image_bytes:\n            QMessageBox.warning(self, \"Export Error\", \"No image is currently loaded.\")\n            return\n\n        # Ask the user where to save the exported image\n        file_name, _ = QFileDialog.getSaveFileName(self, \"Export Image\", \"\",\n                                                   \"PNG (*.png);;JPEG (*.jpg *.jpeg);;All Files (*)\")\n\n        # If a location is chosen, save the image\n        if file_name:\n            with open(file_name, 'wb') as f:\n                f.write(self.original_image_bytes)\n            QMessageBox.information(self, \"Export Success\", \"Image exported successfully!\")\n\n\nclass PDFViewer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.pdf = None\n        self.current_page = 0\n        self.zoom_factor = 1.0\n        self.rotation_angle = 0\n        self.is_panning = False\n        self.pan_start_x = 0\n        self.pan_start_y = 0\n        self.pan_mode = False\n\n        # Optimize performance with page caching\n        self._page_cache = {}  # Cache for rendered pages\n        self._cache_size = 5  # Maximum number of pages to cache\n\n        self.initialize_ui()\n\n    def initialize_ui(self):\n        # Set up the main layout\n        self.layout = QVBoxLayout()\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n        self.layout.setAlignment(Qt.AlignCenter)\n\n        # Create a container for the toolbar and the application viewer\n        container_widget = QWidget(self)\n        container_layout = QVBoxLayout()\n        container_layout.setContentsMargins(0, 0, 0, 0)\n        container_layout.setSpacing(0)\n\n        # Create and set up the toolbar\n        self.setup_toolbar()\n\n        # Add the toolbar to the container layout\n        container_layout.addWidget(self.toolbar)\n\n        # Set up the PDF display area\n        self.setup_pdf_display_area()\n        container_layout.addWidget(self.scroll_area)\n\n        container_widget.setLayout(container_layout)\n        self.layout.addWidget(container_widget)\n\n        self.setLayout(self.layout)\n        self.update_navigation_states()\n\n    def setup_toolbar(self):\n        self.toolbar = QToolBar(self)\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.toolbar.setMovable(False)\n        self.toolbar.setIconSize(QSize(16, 16))  # Reduce icon size\n        self.toolbar.setFixedHeight(32)  # Reduce toolbar height\n        # Disable right click\n        self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)\n\n        # Navigation buttons\n        self.first_action = QAction(QIcon(\"Icons/icons8-thick-arrow-pointing-up-50.png\"), \"First\", self)\n        self.first_action.triggered.connect(self.show_first_page)\n        self.toolbar.addAction(self.first_action)\n\n        self.prev_action = QAction(QIcon(\"Icons/icons8-left-arrow-50.png\"), \"Previous\", self)\n        self.prev_action.triggered.connect(self.show_previous_page)\n        self.toolbar.addAction(self.prev_action)\n\n        # Page entry\n        self.page_entry = QLineEdit(self)\n        self.page_entry.setMaximumWidth(40)\n        self.page_entry.setFixedHeight(22)  # Set fixed height\n        self.page_entry.setAlignment(Qt.AlignRight)\n        self.page_entry.returnPressed.connect(self.go_to_page)\n        self.toolbar.addWidget(self.page_entry)\n\n        # Total pages label\n        self.total_pages_label = QLabel(f\"of {len(self.pdf)}\" if self.pdf else \"of 0\")\n        self.total_pages_label.setFixedHeight(22)  # Set fixed height\n        self.toolbar.addWidget(self.total_pages_label)\n\n        # Navigation buttons\n        self.next_action = QAction(QIcon(\"Icons/icons8-right-arrow-50.png\"), \"Next\", self)\n        self.next_action.triggered.connect(self.show_next_page)\n        self.toolbar.addAction(self.next_action)\n\n        self.last_action = QAction(QIcon(\"Icons/icons8-down-50.png\"), \"Last\", self)\n        self.last_action.triggered.connect(self.show_last_page)\n        self.toolbar.addAction(self.last_action)\n\n        # Add small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        # Zoom actions\n        self.zoom_in_action = QAction(QIcon(\"Icons/icons8-zoom-in-50.png\"), \"Zoom In\", self)\n        self.zoom_in_action.triggered.connect(self.zoom_in)\n        self.toolbar.addAction(self.zoom_in_action)\n\n        # QLineEdit for zoom percentage\n        self.zoom_percentage_entry = QLineEdit(self)\n        self.zoom_percentage_entry.setFixedWidth(60)  # Set a fixed width for consistency\n        self.zoom_percentage_entry.setFixedHeight(22)  # Set fixed height\n        self.zoom_percentage_entry.setAlignment(Qt.AlignRight)\n        self.zoom_percentage_entry.setPlaceholderText(\"100%\")  # Default zoom is 100%\n        self.zoom_percentage_entry.returnPressed.connect(self.set_zoom_from_entry)\n        self.toolbar.addWidget(self.zoom_percentage_entry)\n\n        self.zoom_out_action = QAction(QIcon(\"Icons/icons8-zoom-out-50.png\"), \"Zoom Out\", self)\n        self.zoom_out_action.triggered.connect(self.zoom_out)\n        self.toolbar.addAction(self.zoom_out_action)\n\n        # Create a reset zoom button with its icon and add it to the toolbar\n        reset_zoom_icon = QIcon(\"Icons/icons8-zoom-to-actual-size-50.png\")\n        self.reset_zoom_action = QAction(reset_zoom_icon, \"Reset Zoom\", self)\n        self.reset_zoom_action.triggered.connect(self.reset_zoom)\n        self.toolbar.addAction(self.reset_zoom_action)\n\n        # Add small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        # Fit in window\n        fit_window_icon = QIcon(\"Icons/icons8-enlarge-50.png\")\n        self.fit_window_action = QAction(fit_window_icon, \"Fit in Window\", self)\n        self.fit_window_action.triggered.connect(self.fit_window)\n        self.toolbar.addAction(self.fit_window_action)\n\n        # Fit in width\n        fit_width_icon = QIcon(\"Icons/icons8-resize-horizontal-50.png\")\n        self.fit_width_action = QAction(fit_width_icon, \"Fit in Width\", self)\n        self.fit_width_action.triggered.connect(self.fit_width)\n        self.toolbar.addAction(self.fit_width_action)\n\n        # Add small spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(20, 0)\n        self.toolbar.addWidget(spacer)\n\n        # Pan tool button\n        self.pan_tool_icon = QIcon(\"Icons/icons8-drag-50.png\")\n        self.pan_tool_action = QAction(self.pan_tool_icon, \"Pan Tool\", self)\n        self.pan_tool_action.setCheckable(True)\n        self.pan_tool_action.toggled.connect(self.toggle_pan_mode)\n        self.toolbar.addAction(self.pan_tool_action)\n\n        # Add a spacer to push the following buttons to the right\n        spacer = QWidget(self)\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(spacer)\n\n        # Print button\n        self.print_icon = QIcon(\"Icons/icons8-print-50.png\")\n        self.print_action = QAction(self.print_icon, \"Print\", self)\n        self.print_action.triggered.connect(self.print_pdf)\n        self.toolbar.addAction(self.print_action)\n\n        self.save_pdf_action = QAction(QIcon(\"Icons/icons8-save-as-50.png\"), \"Save PDF\", self)\n        self.save_pdf_action.triggered.connect(self.save_pdf)\n        self.toolbar.addAction(self.save_pdf_action)\n\n    def setup_pdf_display_area(self):\n        self.page_label = QLabel(self)\n        self.page_label.setContentsMargins(0, 0, 0, 0)\n        self.page_label.setAlignment(Qt.AlignCenter)\n\n        self.scroll_area = QScrollArea(self)\n        self.scroll_area.setContentsMargins(0, 0, 0, 0)\n        self.scroll_area.setWidget(self.page_label)\n        self.scroll_area.setWidgetResizable(True)\n\n    def set_current_page(self, page_num):\n        \"\"\"Set the current page and update the view.\"\"\"\n        if not self.pdf:\n            return\n\n        max_pages = len(self.pdf)\n        if 0 <= page_num < max_pages:\n            self.current_page = page_num\n            self.show_page(page_num)\n\n    def go_to_page(self):\n        \"\"\"Navigate to the page entered in the page entry field.\"\"\"\n        try:\n            page_num = int(self.page_entry.text()) - 1  # Minus 1 because pages start from 0\n            self.set_current_page(page_num)\n        except ValueError:\n            QMessageBox.warning(self, \"Invalid Page Number\", \"Please enter a valid page number.\")\n\n    def update_navigation_states(self):\n        \"\"\"Update UI elements based on current PDF and page.\"\"\"\n        if not self.pdf:\n            self.prev_action.setEnabled(False)\n            self.next_action.setEnabled(False)\n            self.first_action.setEnabled(False)\n            self.last_action.setEnabled(False)\n            self.total_pages_label.setText(\"of 0\")\n            self.page_entry.setText(\"\")\n            return\n\n        self.prev_action.setEnabled(self.current_page > 0)\n        self.next_action.setEnabled(self.current_page < len(self.pdf) - 1)\n        self.first_action.setEnabled(self.current_page > 0)\n        self.last_action.setEnabled(self.current_page < len(self.pdf) - 1)\n        self.total_pages_label.setText(f\"of {len(self.pdf)}\")\n        self.page_entry.setText(str(self.current_page + 1))\n\n    def show_previous_page(self):\n        \"\"\"Navigate to the previous page.\"\"\"\n        self.set_current_page(self.current_page - 1)\n        self.update_navigation_states()\n\n    def show_next_page(self):\n        \"\"\"Navigate to the next page.\"\"\"\n        self.set_current_page(self.current_page + 1)\n        self.update_navigation_states()\n\n    def show_page(self, page_num):\n        \"\"\"Display the specified page with caching for better performance.\"\"\"\n        if not self.pdf:\n            return\n\n        try:\n            # Check if the page is in the cache\n            cache_key = (page_num, self.zoom_factor, self.rotation_angle)\n            if cache_key in self._page_cache:\n                # Use cached pixmap\n                self.page_label.setPixmap(self._page_cache[cache_key])\n            else:\n                # Render the page and cache it\n                page = self.pdf[page_num]\n                mat = Matrix(self.zoom_factor, self.zoom_factor).prerotate(self.rotation_angle)\n                image = page.get_pixmap(matrix=mat)\n\n                qt_image = QImage(image.samples, image.width, image.height, image.stride, QImage.Format_RGB888)\n                pixmap = QPixmap.fromImage(qt_image)\n\n                # Cache the pixmap\n                self._page_cache[cache_key] = pixmap\n\n                # Manage cache size\n                if len(self._page_cache) > self._cache_size:\n                    # Remove oldest entry (first key)\n                    oldest_key = next(iter(self._page_cache))\n                    del self._page_cache[oldest_key]\n\n                self.page_label.setPixmap(pixmap)\n\n            self.update_navigation_states()\n        except Exception as e:\n            QMessageBox.critical(self, \"Error\", f\"Failed to render page: {e}\")\n\n    def display(self, content):\n        \"\"\"Load and display a PDF from content bytes.\"\"\"\n        # Clear existing PDF and cache\n        self.clear()\n\n        if content:\n            try:\n                # Try to open PDF directly first\n                self.pdf = fitz_open(stream=content, filetype=\"pdf\")\n                self.current_page = 0\n                self.zoom_factor = 1.0\n                self.rotation_angle = 0\n                self.show_page(self.current_page)\n                self.update_navigation_states()\n            except Exception as e:\n                # If direct open fails, try to clean up the PDF (common with carved files)\n                print(f\"Initial PDF load failed: {e}, attempting cleanup...\")\n                try:\n                    cleaned_content = self.cleanup_pdf_content(content)\n                    self.pdf = fitz_open(stream=cleaned_content, filetype=\"pdf\")\n                    self.current_page = 0\n                    self.zoom_factor = 1.0\n                    self.rotation_angle = 0\n                    self.show_page(self.current_page)\n                    self.update_navigation_states()\n                    print(\"Successfully loaded PDF after cleanup\")\n                except Exception as e2:\n                    print(f\"Failed to load PDF even after cleanup: {e2}\")\n        else:\n            self.page_label.clear()\n\n    @staticmethod\n    def cleanup_pdf_content(content):\n        \"\"\"Clean up PDF content by removing trailing garbage after %%EOF.\n\n        Common issue with carved PDFs - extra bytes after the EOF marker\n        cause PyMuPDF to reject the file even though the PDF is valid.\n        \"\"\"\n        try:\n            # Find the last occurrence of %%EOF\n            eof_marker = b'%%EOF'\n            last_eof = content.rfind(eof_marker)\n\n            if last_eof != -1:\n                # Include the EOF marker plus a small buffer for trailing whitespace\n                # PDF spec allows whitespace/newlines after %%EOF, but not much else\n                end_position = last_eof + len(eof_marker)\n\n                # Look ahead a bit to include any trailing newlines (up to 10 bytes)\n                max_end = min(end_position + 10, len(content))\n                trailing_section = content[end_position:max_end]\n\n                # Count how many whitespace bytes follow EOF\n                whitespace_count = 0\n                for byte in trailing_section:\n                    if byte in (0x0A, 0x0D, 0x20, 0x09):  # \\n, \\r, space, tab\n                        whitespace_count += 1\n                    else:\n                        break\n\n                # Truncate after EOF + whitespace\n                cleaned_content = content[:end_position + whitespace_count]\n\n                print(f\"PDF cleanup: Truncated {len(content) - len(cleaned_content)} trailing bytes\")\n                return cleaned_content\n            else:\n                # No EOF marker found, return original\n                print(\"PDF cleanup: No %%EOF marker found, returning original content\")\n                return content\n\n        except Exception as e:\n            print(f\"Error during PDF cleanup: {e}\")\n            return content\n\n    def clear(self):\n        \"\"\"Close the PDF and clear all resources.\"\"\"\n        if self.pdf:\n            self.pdf.close()\n            self.pdf = None\n\n        # Clear the cache\n        self._page_cache.clear()\n        self.page_label.clear()\n        self.update_navigation_states()\n\n    def show_first_page(self):\n        \"\"\"Navigate to the first page.\"\"\"\n        self.set_current_page(0)\n\n    def show_last_page(self):\n        \"\"\"Navigate to the last page.\"\"\"\n        if self.pdf:\n            self.set_current_page(len(self.pdf) - 1)\n\n    def zoom_in(self):\n        \"\"\"Increase zoom level.\"\"\"\n        # Don't allow extreme zoom levels\n        if self.zoom_factor < 5.0:\n            self.zoom_factor *= 1.2\n            # Clear cache on zoom change\n            self._page_cache.clear()\n            self.show_page(self.current_page)\n            # Update zoom display\n            self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n\n    def zoom_out(self):\n        \"\"\"Decrease zoom level.\"\"\"\n        # Don't allow extreme zoom levels\n        if self.zoom_factor > 0.1:\n            self.zoom_factor *= 0.8\n            # Clear cache on zoom change\n            self._page_cache.clear()\n            self.show_page(self.current_page)\n            # Update zoom display\n            self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n\n    def set_zoom_from_entry(self):\n        \"\"\"Set zoom level from the entry field.\"\"\"\n        try:\n            # Extract the percentage from the QLineEdit\n            text = self.zoom_percentage_entry.text().strip('%')\n            percentage = float(text) / 100\n\n            if 0.1 <= percentage <= 5:  # Enforce reasonable zoom limits\n                self.zoom_factor = percentage\n                # Clear cache on zoom change\n                self._page_cache.clear()\n                self.show_page(self.current_page)\n            else:\n                QMessageBox.warning(self, \"Invalid Zoom\", \"Please enter a zoom percentage between 10% and 500%.\")\n                # Reset the entry to the current zoom\n                self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n        except ValueError:\n            QMessageBox.warning(self, \"Invalid Zoom\", \"Please enter a valid zoom percentage.\")\n            # Reset the entry to the current zoom\n            self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n\n    def reset_zoom(self):\n        \"\"\"Reset zoom to original size.\"\"\"\n        self.zoom_factor = 1.0\n        # Clear cache on zoom change\n        self._page_cache.clear()\n        self.show_page(self.current_page)\n        self.zoom_percentage_entry.setText(\"100%\")\n\n    def fit_window(self):\n        \"\"\"Adjust zoom to fit the entire page in the window.\"\"\"\n        if not self.pdf or self.current_page >= len(self.pdf):\n            return\n\n        page = self.pdf[self.current_page]\n        zoom_x = self.scroll_area.width() / page.rect.width\n        zoom_y = self.scroll_area.height() / page.rect.height\n        self.zoom_factor = min(zoom_x, zoom_y) * 0.95  # 95% to add a small margin\n        # Clear cache on zoom change\n        self._page_cache.clear()\n        self.show_page(self.current_page)\n        # Update zoom display\n        self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n\n    def fit_width(self):\n        \"\"\"Adjust zoom to fit the page width in the window.\"\"\"\n        if not self.pdf or self.current_page >= len(self.pdf):\n            return\n\n        page = self.pdf[self.current_page]\n        self.zoom_factor = self.scroll_area.width() / page.rect.width * 0.95  # 95% to add a small margin\n        # Clear cache on zoom change\n        self._page_cache.clear()\n        self.show_page(self.current_page)\n        # Update zoom display\n        self.zoom_percentage_entry.setText(f\"{int(self.zoom_factor * 100)}%\")\n\n    def rotate_left(self):\n        \"\"\"Rotate the page 90 degrees counterclockwise.\"\"\"\n        self.rotation_angle -= 90\n        # Clear cache on rotation change\n        self._page_cache.clear()\n        self.show_page(self.current_page)\n\n    def rotate_right(self):\n        \"\"\"Rotate the page 90 degrees clockwise.\"\"\"\n        self.rotation_angle += 90\n        # Clear cache on rotation change\n        self._page_cache.clear()\n        self.show_page(self.current_page)\n\n    def toggle_pan_mode(self, checked):\n        \"\"\"Enable or disable panning mode.\"\"\"\n        self.pan_mode = checked\n        self.setCursor(Qt.OpenHandCursor if checked else Qt.ArrowCursor)\n\n    def mousePressEvent(self, event):\n        \"\"\"Handle mouse press events for panning.\"\"\"\n        if event.button() == Qt.LeftButton and self.pan_mode:\n            self.is_panning = True\n            self.pan_start_x = event.x()\n            self.pan_start_y = event.y()\n            self.setCursor(Qt.ClosedHandCursor)  # Change to closed hand cursor while panning\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Handle mouse move events for panning.\"\"\"\n        if self.is_panning and self.pan_mode:\n            # Calculate the distance moved\n            dx = event.x() - self.pan_start_x\n            dy = event.y() - self.pan_start_y\n\n            # Update scroll position\n            self.scroll_area.horizontalScrollBar().setValue(self.scroll_area.horizontalScrollBar().value() - dx)\n            self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().value() - dy)\n\n            # Update the mouse position for the next move\n            self.pan_start_x = event.x()\n            self.pan_start_y = event.y()\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        \"\"\"Handle mouse release events for panning.\"\"\"\n        if event.button() == Qt.LeftButton and self.is_panning and self.pan_mode:\n            self.is_panning = False\n            self.setCursor(Qt.OpenHandCursor)  # Change back to open hand cursor\n        event.accept()\n\n    def print_pdf(self):\n        \"\"\"Print the current PDF.\"\"\"\n        if not self.pdf:\n            QMessageBox.warning(self, \"No Document\", \"No document available to print.\")\n            return\n\n        printer = QPrinter()\n        printer.setFullPage(True)\n        printer.setPageOrientation(QPageLayout.Portrait)\n\n        print_dialog = QPrintDialog(printer, self)\n        if print_dialog.exec_() == QPrintDialog.Accepted:\n            from PySide6.QtGui import QPainter\n\n            try:\n                painter = QPainter()\n                if not painter.begin(printer):\n                    QMessageBox.critical(self, \"Error\", \"Failed to initialize printer.\")\n                    return\n\n                num_pages = len(self.pdf)\n                for i in range(num_pages):\n                    if i != 0:  # start a new page after the first one\n                        printer.newPage()\n\n                    # Render the page at a higher resolution for printing\n                    page = self.pdf[i]\n                    image = page.get_pixmap(matrix=Matrix(2.0, 2.0))  # Higher resolution for print\n\n                    qt_image = QImage(image.samples, image.width, image.height, image.stride, QImage.Format_RGB888)\n                    pixmap = QPixmap.fromImage(qt_image)\n\n                    # Scale to printer page\n                    rect = painter.viewport()\n                    size = pixmap.size()\n                    size.scale(rect.size(), Qt.KeepAspectRatio)\n                    painter.setViewport(rect.x(), rect.y(), size.width(), size.height())\n                    painter.setWindow(pixmap.rect())\n                    painter.drawPixmap(0, 0, pixmap)\n\n                painter.end()\n                QMessageBox.information(self, \"Print Complete\", \"Document was sent to the printer.\")\n            except Exception as e:\n                QMessageBox.critical(self, \"Error\", f\"Failed to print document: {e}\")\n                if painter.isActive():\n                    painter.end()\n\n    def save_pdf(self):\n        \"\"\"Save the current PDF to a file.\"\"\"\n        if not self.pdf:\n            QMessageBox.warning(self, \"No Document\", \"No document available to save.\")\n            return\n\n        options = QFileDialog.Options()\n        filePath, _ = QFileDialog.getSaveFileName(self, \"Save PDF\", \"\", \"PDF Files (*.pdf);;All Files (*)\",\n                                                  options=options)\n\n        if not filePath:\n            return  # user cancelled the dialog\n\n        if not filePath.endswith(\".pdf\"):\n            filePath += \".pdf\"\n\n        try:\n            self.pdf.save(filePath)  # save the PDF to the specified path\n            QMessageBox.information(self, \"Success\", \"PDF saved successfully!\")\n        except Exception as e:\n            QMessageBox.critical(self, \"Error\", f\"Failed to save PDF: {e}\")\n\n\nclass AudioVideoPlayer(QWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.parent = parent\n\n        # Initialize attributes before calling methods that use them\n        self._is_playing = False\n        self._current_volume = 50  # Default volume level\n        self._is_muted = False\n        self._previous_volume = 50  # Store previous volume when muting\n        self._audio_session = None\n        self._volume_interface = None\n        self._is_audio_only = False  # Flag to track if we're playing audio-only content\n        self._shutting_down = False  # Flag to indicate shutdown in progress\n\n        # Now initialize UI and connections\n        self.initialize_ui()\n        self.setup_connections()\n        self._setup_os_volume()\n\n    def initialize_ui(self):\n        # Main layout\n        self.layout = QVBoxLayout(self)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n        self.layout.setSpacing(0)\n\n        # Create video widget\n        self.video_widget = QVideoWidget(self)\n        self.video_widget.setMinimumSize(QSize(400, 300))\n\n        # Create media player\n        self.media_player = QMediaPlayer(self)\n        self.audio_output = QAudioOutput(self)\n        self.media_player.setVideoOutput(self.video_widget)\n        self.media_player.setAudioOutput(self.audio_output)\n\n        # Create label to display when playing audio-only content\n        self.audio_label = QLabel(\"Playing Audio\", self)\n        self.audio_label.setObjectName(\"audioOnlyLabel\")  # For stylesheet targeting\n        self.audio_label.setAlignment(Qt.AlignCenter)\n        self.audio_label.setVisible(False)\n\n        # Set default volume\n        self.audio_output.setVolume(self._current_volume / 100.0)\n\n        # Create controls\n        self.create_controls()\n\n        # Add widgets to layout\n        self.layout.addWidget(self.video_widget)\n        self.layout.addWidget(self.audio_label)\n        self.layout.addWidget(self.control_widget)\n\n    def set_audio_only_mode(self, is_audio_only=True):\n        \"\"\"Configure the player for audio-only content\"\"\"\n        self._is_audio_only = is_audio_only\n        self.video_widget.setVisible(not is_audio_only)\n        self.audio_label.setVisible(is_audio_only)\n\n        # Set the media player flags accordingly\n        try:\n            if hasattr(self.media_player, 'setOption'):\n                if is_audio_only:\n                    # For audio-only content, set flags that optimize for audio playback\n                    self.media_player.setOption(\"audio-only\", \"true\")\n                    self.media_player.setOption(\"skip-video\", \"true\")\n                else:\n                    # Reset flags for video content\n                    self.media_player.setOption(\"audio-only\", \"false\")\n                    self.media_player.setOption(\"skip-video\", \"false\")\n        except Exception as e:\n            print(f\"Warning: Could not set audio-only mode options: {e}\")\n\n    def handle_media_status_change(self, status):\n        \"\"\"Handle media status changes\"\"\"\n        # If this is an audio file and we see no video streams, switch to audio-only mode\n        try:\n            if status == QMediaPlayer.LoadedMedia:\n                # Check if we can detect if this is audio-only content\n                has_video = False\n\n                if hasattr(self.media_player, 'hasVideo'):\n                    has_video = self.media_player.hasVideo()\n\n                # Set the appropriate mode\n                self.set_audio_only_mode(not has_video)\n        except Exception as e:\n            print(f\"Warning: Error detecting audio/video mode: {e}\")\n\n    def setup_connections(self):\n        # Media player signals (updated for newer API)\n        self.media_player.errorOccurred.connect(self.handle_error)\n        self.media_player.positionChanged.connect(self.update_position)\n        self.media_player.durationChanged.connect(self.update_duration)\n        self.media_player.playbackStateChanged.connect(self.update_play_state)\n\n        # Media status signals - if available in this version\n        if hasattr(self.media_player, 'mediaStatusChanged'):\n            self.media_player.mediaStatusChanged.connect(self.handle_media_status_change)\n\n        # Control signals\n        self.play_button.clicked.connect(self.toggle_play)\n        self.stop_button.clicked.connect(self.stop)\n        self.position_slider.sliderMoved.connect(self.set_position)\n        self.volume_button.clicked.connect(self.toggle_mute)\n        self.volume_slider.valueChanged.connect(self.set_volume)\n\n    def _setup_os_volume(self):\n        \"\"\"Set up OS-specific volume control (Windows only)\"\"\"\n        if platform.system() == \"Windows\":\n            try:\n                from comtypes import CLSCTX_ALL\n                from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume\n\n                devices = AudioUtilities.GetSpeakers()\n                self._audio_session = AudioUtilities.GetAllSessions()\n                interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)\n                self._volume_interface = cast(interface, POINTER(IAudioEndpointVolume))\n            except Exception as e:\n                print(f\"Could not initialize Windows audio integration: {e}\")\n\n    def set_os_volume(self, volume_level):\n        \"\"\"Set system volume (Windows only)\"\"\"\n        if self._volume_interface and platform.system() == \"Windows\":\n            try:\n                # Convert from 0-100 to 0.0-1.0 range\n                self._volume_interface.SetMasterVolumeLevelScalar(volume_level / 100.0, None)\n            except Exception as e:\n                print(f\"Error setting system volume: {e}\")\n\n    def toggle_play(self):\n        if self._is_playing:\n            self.media_player.pause()\n        else:\n            self.media_player.play()\n\n    def stop(self):\n        \"\"\"Stop playback and reset position\"\"\"\n        try:\n            if hasattr(self, 'media_player') and self.media_player:\n                self.media_player.stop()\n                self._is_playing = False\n                self.update_controls()\n        except Exception as e:\n            print(f\"Error stopping media playback: {e}\")\n\n    def update_play_state(self, state):\n        # Updated for newer API\n        self._is_playing = (state == QMediaPlayer.PlayingState)\n        self.update_controls()\n\n    def update_controls(self):\n        if self._is_playing:\n            # Try different pause icon paths\n            pause_icon_paths = [\n                \"Icons/icons8-pause-50.png\",\n                \"Icons/pause.png\",\n                \"Icons/icons8-pause-button-50.png\"\n            ]\n            icon_set = False\n            for path in pause_icon_paths:\n                if os.path.exists(path):\n                    self.play_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.play_button.setText(\"Pause\")\n        else:\n            # Try different play icon paths\n            play_icon_paths = [\n                \"Icons/icons8-play-50.png\",\n                \"Icons/play.png\",\n                \"Icons/icons8-circled-play-50.png\"\n            ]\n            icon_set = False\n            for path in play_icon_paths:\n                if os.path.exists(path):\n                    self.play_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.play_button.setText(\"Play\")\n\n    def set_position(self, position):\n        self.media_player.setPosition(position)\n\n    def update_position(self, position):\n        # Block signals to prevent slider feedback loops\n        self.position_slider.blockSignals(True)\n        self.position_slider.setValue(position)\n        self.position_slider.blockSignals(False)\n\n        # Update time label\n        self.current_time_label.setText(self.format_time(position))\n\n    def update_duration(self, duration):\n        self.position_slider.setRange(0, duration)\n        self.total_time_label.setText(self.format_time(duration))\n\n    def format_time(self, milliseconds):\n        seconds = milliseconds // 1000\n        minutes = seconds // 60\n        seconds %= 60\n        return f\"{minutes:02d}:{seconds:02d}\"\n\n    def toggle_mute(self):\n        self._is_muted = not self._is_muted\n        if self._is_muted:\n            self._previous_volume = self._current_volume\n            self.set_volume(0)\n            # Try different mute icon paths\n            mute_icon_paths = [\n                \"Icons/icons8-mute-50.png\",\n                \"Icons/mute.png\"\n            ]\n            icon_set = False\n            for path in mute_icon_paths:\n                if os.path.exists(path):\n                    self.volume_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.volume_button.setText(\"Mute\")\n        else:\n            self.set_volume(self._previous_volume)\n            # Try different volume icon paths\n            volume_icon_paths = [\n                \"Icons/icons8-audio-50.png\",\n                \"Icons/volume.png\",\n                \"Icons/audio.png\"\n            ]\n            icon_set = False\n            for path in volume_icon_paths:\n                if os.path.exists(path):\n                    self.volume_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.volume_button.setText(\"Vol\")\n\n        # Update system volume if enabled\n        self.set_os_volume(self._current_volume)\n\n    def set_volume(self, volume):\n        self._current_volume = volume\n        self.audio_output.setVolume(volume / 100.0)\n\n        # Update volume slider\n        self.volume_slider.blockSignals(True)\n        self.volume_slider.setValue(volume)\n        self.volume_slider.blockSignals(False)\n\n        # Update mute button icon based on volume\n        if volume == 0:\n            self._is_muted = True\n            # Try different mute icon paths\n            mute_icon_paths = [\n                \"Icons/icons8-mute-50.png\",\n                \"Icons/mute.png\"\n            ]\n            icon_set = False\n            for path in mute_icon_paths:\n                if os.path.exists(path):\n                    self.volume_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.volume_button.setText(\"Mute\")\n        else:\n            self._is_muted = False\n            # Try different volume icon paths\n            volume_icon_paths = [\n                \"Icons/icons8-audio-50.png\",\n                \"Icons/volume.png\",\n                \"Icons/audio.png\"\n            ]\n            icon_set = False\n            for path in volume_icon_paths:\n                if os.path.exists(path):\n                    self.volume_button.setIcon(QIcon(path))\n                    icon_set = True\n                    break\n\n            if not icon_set:\n                self.volume_button.setText(\"Vol\")\n\n        # Update system volume if enabled\n        self.set_os_volume(volume)\n\n    def handle_error(self, error, error_string):\n        if error != QMediaPlayer.NoError:\n            QMessageBox.warning(self, \"Media Error\", f\"Error: {error_string}\")\n\n    def closeEvent(self, event):\n        # Clean up resources\n        try:\n            if hasattr(self, 'media_player') and self.media_player:\n                self.media_player.stop()\n        except Exception as e:\n            print(f\"Error stopping media player during close: {e}\")\n        super().closeEvent(event)\n\n    def __del__(self):\n        # Clean up any lingering resources\n        try:\n            if not hasattr(self, '_shutting_down') or not self._shutting_down:\n                self.safe_stop()\n        except Exception:\n            # Silently ignore errors during destruction\n            pass\n\n    def create_controls(self):\n        # Control widget and layout\n        self.control_widget = QWidget(self)\n        self.control_layout = QHBoxLayout(self.control_widget)\n        self.control_layout.setContentsMargins(2, 2, 2, 2)\n        self.control_layout.setSpacing(2)\n\n        # Play/Pause button with fallback icon paths\n        self.play_button = QPushButton(self)\n        # Try different icon paths\n        play_icon_paths = [\n            \"Icons/icons8-play-50.png\",\n            \"Icons/play.png\",\n            \"Icons/icons8-circled-play-50.png\"\n        ]\n        for path in play_icon_paths:\n            if os.path.exists(path):\n                self.play_button.setIcon(QIcon(path))\n                break\n        else:\n            # Fallback - create a text button\n            self.play_button.setText(\"Play\")\n\n        self.play_button.setIconSize(QSize(16, 16))\n        self.play_button.setFixedHeight(22)\n        self.play_button.setFlat(True)\n        self.play_button.setToolTip(\"Play/Pause\")\n\n        # Stop button with fallback icon paths\n        self.stop_button = QPushButton(self)\n        # Try different icon paths\n        stop_icon_paths = [\n            \"Icons/icons8-stop-50.png\",\n            \"Icons/stop.png\",\n            \"Icons/icons8-stop-circled-50.png\"\n        ]\n        for path in stop_icon_paths:\n            if os.path.exists(path):\n                self.stop_button.setIcon(QIcon(path))\n                break\n        else:\n            # Fallback - create a text button\n            self.stop_button.setText(\"Stop\")\n\n        self.stop_button.setIconSize(QSize(16, 16))\n        self.stop_button.setFixedHeight(22)\n        self.stop_button.setFlat(True)\n        self.stop_button.setToolTip(\"Stop\")\n\n        # Position slider\n        self.position_slider = QSlider(Qt.Horizontal, self)\n        self.position_slider.setFixedHeight(22)\n        self.position_slider.setRange(0, 0)  # Will be updated when media is loaded\n        self.position_slider.setToolTip(\"Position\")\n\n        # Time labels\n        self.current_time_label = QLabel(\"00:00\", self)\n        self.current_time_label.setFixedHeight(22)\n        self.current_time_label.setMinimumWidth(40)\n        self.total_time_label = QLabel(\"00:00\", self)\n        self.total_time_label.setFixedHeight(22)\n        self.total_time_label.setMinimumWidth(40)\n\n        # Volume button with fallback icon paths\n        self.volume_button = QPushButton(self)\n        # Try different icon paths\n        volume_icon_paths = [\n            \"Icons/icons8-audio-50.png\",\n            \"Icons/volume.png\",\n            \"Icons/audio.png\"\n        ]\n        for path in volume_icon_paths:\n            if os.path.exists(path):\n                self.volume_button.setIcon(QIcon(path))\n                break\n        else:\n            # Fallback - create a text button\n            self.volume_button.setText(\"Vol\")\n\n        self.volume_button.setIconSize(QSize(16, 16))\n        self.volume_button.setFixedHeight(22)\n        self.volume_button.setFlat(True)\n        self.volume_button.setToolTip(\"Mute/Unmute\")\n\n        self.volume_slider = QSlider(Qt.Horizontal, self)\n        self.volume_slider.setFixedHeight(22)\n        self.volume_slider.setRange(0, 100)\n        self.volume_slider.setValue(self._current_volume)\n        self.volume_slider.setMaximumWidth(80)\n        self.volume_slider.setToolTip(\"Volume\")\n\n        # Add controls to layout\n        self.control_layout.addWidget(self.play_button)\n        self.control_layout.addWidget(self.stop_button)\n        self.control_layout.addWidget(self.current_time_label)\n        self.control_layout.addWidget(self.position_slider)\n        self.control_layout.addWidget(self.total_time_label)\n        self.control_layout.addWidget(self.volume_button)\n        self.control_layout.addWidget(self.volume_slider)\n\n    def safe_stop(self):\n        \"\"\"Safely stop playback even during shutdown.\"\"\"\n        try:\n            if hasattr(self, 'media_player') and self.media_player:\n                # Set flag to indicate we're shutting down\n                self._shutting_down = True\n\n                # Stop playback\n                self.media_player.stop()\n\n                # Process events to ensure stop command is processed\n                QApplication.processEvents()\n\n                # Release audio output\n                if hasattr(self, 'audio_output') and self.audio_output:\n                    # Remove it from the media player first\n                    if hasattr(self.media_player, 'setAudioOutput'):\n                        try:\n                            self.media_player.setAudioOutput(None)\n                            # Process events to ensure this is applied\n                            QApplication.processEvents()\n                        except Exception as e:\n                            print(f\"Error removing audio output: {e}\")\n\n                # Set media to null/empty to release resources\n                if hasattr(self.media_player, 'setSource'):\n                    try:\n                        self.media_player.setSource(QUrl())\n                        # Process events to ensure this is applied\n                        QApplication.processEvents()\n                    except Exception as e:\n                        print(f\"Error clearing media source: {e}\")\n\n                # Wait a moment for resources to be released\n                time.sleep(0.1)\n        except Exception as e:\n            print(f\"Error in safe_stop: {e}\")\n"
  },
  {
    "path": "modules/verification.py",
    "content": "from PySide6.QtGui import QIcon, QFont\nfrom PySide6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QPushButton, QApplication, QProgressBar, QHBoxLayout,\n                               QFileDialog, QTextEdit)\nfrom PySide6.QtCore import QThread, Signal, Qt\n\n\nclass HashCalculationThread(QThread):\n    hashCalculated = Signal(dict)  # Signal for hash results\n    progressUpdated = Signal(float)  # Signal for progress updates (percentage 0-100)\n\n    def __init__(self, image_handler):\n        super().__init__()\n        self.image_handler = image_handler\n        self.isRunning = True\n\n    def run(self):\n        try:\n            # Pass a progress callback to update the progress bar\n            hash_results = self.image_handler.calculate_hashes(\n                progress_callback=self.update_progress\n            )\n            if self.isRunning:  # Check if we're still running before emitting the signal\n                self.hashCalculated.emit(hash_results)\n        except Exception as e:\n            print(f\"Error in hash calculation thread: {e}\")\n            if self.isRunning:\n                self.hashCalculated.emit({})  # Empty dict indicates error\n\n    def update_progress(self, current, total):\n        \"\"\"Handle progress updates safely with large values.\"\"\"\n        try:\n            if total > 0 and self.isRunning:\n                # Convert to float to avoid overflow and limit to 0-100 range\n                percentage = min(100.0, (float(current) / float(total)) * 100.0)\n                self.progressUpdated.emit(percentage)\n        except Exception as e:\n            print(f\"Progress update error: {e}\")\n\n    def stop(self):\n        \"\"\"Safely stop the thread.\"\"\"\n        self.isRunning = False\n\n\nclass VerificationWidget(QWidget):\n    def __init__(self, image_handler, parent=None):\n        super().__init__(parent)\n        self.image_handler = image_handler\n        self.thread = None\n        self.setWindowTitle(\"Trace - Image Verification\")\n        self.setWindowIcon(QIcon('Icons/logo.png'))\n        self.setGeometry(100, 100, 750, 400)  # Adjust size for better layout\n        self._verified = False  # Track verification status\n\n        layout = QVBoxLayout(self)\n        layout.setContentsMargins(20, 20, 20, 20)\n\n        self.software_info = QLabel(\"Trace - Forensic Analysis Tool\", self)\n        self.software_info.setObjectName(\"softwareInfoLabel\")\n        layout.addWidget(self.software_info)\n\n        self.subtitle = QLabel(\"Image Hash Verification\", self)\n        self.subtitle.setObjectName(\"subtitleLabel\")\n        layout.addWidget(self.subtitle)\n\n        self.hash_label = QTextEdit(\"Calculating hashes...\")\n        self.hash_label.setReadOnly(True)\n        self.hash_label.setFont(QFont(\"Courier\", 10))\n        self.hash_label.setStyleSheet(\"\"\"\n            QTextEdit {\n                background-color: #f0f0f0;\n                border: 1px solid #ccc;\n                color: #333;\n                font-family: 'Courier';\n            }\n            QTextEdit::indicator:checked {\n                background: #b0b0b0;\n            }\n        \"\"\")\n        layout.addWidget(self.hash_label)\n\n        progress_bar_container = QHBoxLayout()\n        progress_bar_container.addStretch()\n\n        self.progress_bar = QProgressBar()\n        self.progress_bar.setMinimum(0)\n        self.progress_bar.setMaximum(100)  # Set to 100 for percentage display\n        self.progress_bar.setFixedWidth(400)\n        self.progress_bar.setAlignment(Qt.AlignCenter)\n        self.progress_bar.setStyleSheet(\"\"\"\n            QProgressBar {\n                border: 2px solid grey;\n                border-radius: 5px;\n                text-align: center;\n            }\n            QProgressBar::chunk {\n                background-color: #05B8CC;\n                width: 20px;\n            }\n        \"\"\")\n        progress_bar_container.addWidget(self.progress_bar)\n        progress_bar_container.addStretch()\n        layout.addLayout(progress_bar_container)\n\n        button_layout = QHBoxLayout()\n        self.save_button = QPushButton(\"Save to Text File\", self)\n        self.save_button.setFixedWidth(150)\n        self.save_button.clicked.connect(self.save_hash)\n        self.save_button.setEnabled(False)\n        button_layout.addWidget(self.save_button)\n\n        self.copy_button = QPushButton(\"Copy\", self)\n        self.copy_button.setFixedWidth(150)\n        self.copy_button.clicked.connect(self.copy_hash)\n        self.copy_button.setEnabled(False)\n        button_layout.addWidget(self.copy_button)\n\n        self.close_button = QPushButton(\"Close\", self)\n        self.close_button.setFixedWidth(150)\n        self.close_button.clicked.connect(self.close)\n        button_layout.addWidget(self.close_button)\n        layout.addLayout(button_layout)\n\n        # Start hash calculation with a slight delay to allow the UI to initialize\n        QApplication.processEvents()\n        self.start_hash_calculation()\n\n    def closeEvent(self, event):\n        \"\"\"Override closeEvent to properly clean up resources.\"\"\"\n        if self.thread and self.thread.isRunning():\n            self.thread.stop()  # Tell thread to stop processing\n            self.thread.wait(1000)  # Wait up to 1 second\n\n            # If thread is still running, terminate it\n            if self.thread.isRunning():\n                self.thread.terminate()\n                self.thread.wait()\n\n        super().closeEvent(event)\n\n    def save_hash(self):\n        file_name, _ = QFileDialog.getSaveFileName(self, \"Save Hash\", \"\", \"Text Files (*.txt)\")\n        if file_name:\n            with open(file_name, 'w') as file:\n                file.write(self.hash_label.toPlainText())\n\n    def start_hash_calculation(self):\n        # Clean up any previous thread\n        if self.thread and self.thread.isRunning():\n            self.thread.stop()\n            self.thread.wait()\n\n        self.thread = HashCalculationThread(self.image_handler)\n        self.thread.hashCalculated.connect(self.on_hash_calculated)\n        self.thread.progressUpdated.connect(self.update_progress)\n        self.thread.start()\n\n    def update_progress(self, percentage):\n        \"\"\"Update progress bar with the given percentage.\"\"\"\n        try:\n            self.progress_bar.setValue(int(percentage))\n            QApplication.processEvents()  # Keep UI responsive\n        except Exception as e:\n            print(f\"Error updating progress bar: {e}\")\n\n    def on_hash_calculated(self, hash_results):\n        \"\"\"Process hash results and update UI.\"\"\"\n        try:\n            # Set the progress bar to 100% complete\n            self.progress_bar.setValue(100)\n\n            if hash_results and 'computed_md5' in hash_results:\n                verification_results = []\n\n                computed_md5 = hash_results.get('computed_md5')\n                computed_sha1 = hash_results.get('computed_sha1')\n                computed_sha256 = hash_results.get('computed_sha256')\n\n                # Check if the loaded image file is of E01 format\n                if self.image_handler and self.image_handler.get_image_type() == \"ewf\":\n                    stored_md5 = hash_results.get('stored_md5')\n                    stored_sha1 = hash_results.get('stored_sha1')\n\n                    # Compare the computed MD5 and SHA1 hashes with the stored hashes\n                    md5_result = \"Match\" if computed_md5 == stored_md5 else \"Mismatch\"\n                    sha1_result = \"Match\" if computed_sha1 == stored_sha1 else \"Mismatch\"\n\n                    # Set verification status\n                    self._verified = md5_result == \"Match\" or sha1_result == \"Match\"\n\n                    verification_results.append(f\"<b>Stored MD5:</b> {stored_md5 or 'N/A'}\")\n                    verification_results.append(f\"<b>Computed MD5:</b> {computed_md5}\")\n                    verification_results.append(\n                        f\"<b>MD5 Verify result:</b> {md5_result}<br>\")  # New line after MD5 verification result\n\n                    verification_results.append(f\"<b>Stored SHA1:</b> {stored_sha1 or 'N/A'}\")\n                    verification_results.append(f\"<b>Computed SHA1:</b> {computed_sha1}\")\n                    verification_results.append(\n                        f\"<b>SHA1 Verify result:</b> {sha1_result}<br>\")  # New line after SHA1 verification result\n\n                else:  # For other image types, only display computed hashes\n                    verification_results.append(f\"<b>Computed MD5:</b> {computed_md5}\")\n                    verification_results.append(f\"<b>Computed SHA1:</b> {computed_sha1}\")\n\n                # Display computed SHA256 hash for all image types\n                verification_results.append(f\"<b>Computed SHA256:</b> {computed_sha256}\")\n\n                # Convert size from bytes to megabytes\n                size_bytes = hash_results.get('size')\n                size_mb = size_bytes / (1024 * 1024)\n\n                hash_info = \"<br>\".join(verification_results)\n                hash_info += f\"<br><br><b>Size:</b> {size_bytes} bytes ({size_mb:.2f} MB)<br><b>Path:</b> {hash_results.get('path')}\"\n                self.hash_label.setHtml(hash_info)\n                self.save_button.setEnabled(True)\n                self.copy_button.setEnabled(True)\n            else:\n                self.hash_label.setText(\"Error calculating hashes. Please ensure the image is accessible.\")\n        except Exception as e:\n            print(f\"Error processing hash results: {e}\")\n            self.hash_label.setText(f\"Error processing results: {str(e)}\")\n\n    def copy_hash(self):\n        clipboard = QApplication.clipboard()\n        clipboard.setText(self.hash_label.toPlainText())\n\n    @property\n    def is_verified(self):\n        # Return the verification status property\n        return self._verified\n"
  },
  {
    "path": "modules/veriphone_api.py",
    "content": "import requests\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QIcon\nfrom PySide6.QtWidgets import (QWidget, QVBoxLayout, QPushButton, QTextBrowser, QLineEdit, QLabel, QToolBar,\n                               QSizePolicy, QMessageBox)\n\n\nclass VeriphoneWidget(QWidget):\n    def __init__(self):\n        super().__init__()\n        self.api_key = None\n        self.init_ui()\n\n    def init_ui(self):\n        self.layout = QVBoxLayout(self)\n        self.layout.setSpacing(0)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n\n        # set widget size but make it resizable\n        self.setFixedSize(600, 400)\n\n        # set window title\n        self.setWindowTitle(\"Veriphone Phone Number Verification\")\n\n        # add icon to the window\n        self.setWindowIcon(QIcon('Icons/logo.png'))\n\n        # Toolbar setup\n        self.toolbar = QToolBar(\"Veriphone Toolbar\", self)\n        self.toolbar.setContentsMargins(0, 0, 0, 0)\n        self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        self.layout.addWidget(self.toolbar)\n\n        # Phone input field\n        self.phone_input = QLineEdit(self)\n        # size of the input field\n        self.phone_input.setFixedSize(300, 30)\n        self.phone_input.setPlaceholderText(\"Enter phone number with country code\")\n        self.toolbar.addWidget(self.phone_input)\n        # Connect returnPressed signal to verify_phone_number method\n        self.phone_input.returnPressed.connect(self.verify_phone_number)\n\n        # spacer\n        spacer = QWidget(self)\n        spacer.setFixedSize(10, 10)\n        self.toolbar.addWidget(spacer)\n\n        # Verify button in toolbar\n        verify_button = QPushButton(\"Verify\", self)\n        verify_button.clicked.connect(self.verify_phone_number)\n        self.toolbar.addWidget(verify_button)\n\n        # Spacer widget to push the logo to the far right\n        spacer = QWidget(self)\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        self.toolbar.addWidget(spacer)\n\n        # Logo on the far right\n        self.logo_label = QLabel(self)\n        self.logo_pixmap = QPixmap(\"Icons/logo_veriphone.png\")  # Make sure the path is correct\n        self.logo_label.setPixmap(self.logo_pixmap.scaled(120, 70, Qt.KeepAspectRatio,\n                                                          Qt.SmoothTransformation))  # Adjust 100x50 to your desired size\n        self.toolbar.addWidget(self.logo_label)\n\n        # Text browser for showing the results\n        self.info_text_edit = QTextBrowser(self)\n        self.info_text_edit.setReadOnly(True)\n        self.layout.addWidget(self.info_text_edit)\n\n    def set_api_key(self, key):\n        self.api_key = key\n\n    def use_api_key(self):\n        if not self.api_key:\n            raise ValueError(\"API key not set\")\n\n    def verify_phone_number(self):\n        if not self.api_key:\n            QMessageBox.warning(self, \"API Key Not Set\",\n                                \"Please set the API key in the Options menu before verifying a phone number.\")\n            return\n\n        phone_number = self.phone_input.text()\n        if phone_number:\n            self.update_veriphone_info(phone_number)\n        else:\n            QMessageBox.warning(self, \"Input Error\", \"Please enter a phone number to verify.\")\n\n    def update_veriphone_info(self, phone_number):\n        data = self.verify_phone_with_veriphone(phone_number)\n        if data.get('status') == 'success':\n            info_text = self.format_data_as_html(data)\n            self.info_text_edit.setHtml(info_text)\n        else:\n            self.info_text_edit.setText(\"Failed to fetch data or phone number is invalid.\")\n\n    def verify_phone_with_veriphone(self, phone_number):\n        url = f\"https://api.veriphone.io/v2/verify?phone={phone_number}&key={self.api_key}\"\n        response = requests.get(url)\n        if response.status_code == 200:\n            return response.json()\n        else:\n            return {\"status\": \"error\", \"message\": \"Failed to verify phone number.\"}\n\n    def format_data_as_html(self, data):\n        # Additional fields from the Veriphone API\n        phone_region = data.get('phone_region', 'N/A')\n        country = data.get('country', 'N/A')\n        country_code = data.get('country_code', 'N/A')\n        country_prefix = data.get('country_prefix', 'N/A')\n        international_number = data.get('international_number', 'N/A')\n        local_number = data.get('local_number', 'N/A')\n        e164 = data.get('e164', 'N/A')\n        carrier = data.get('carrier', 'N/A')\n\n        html_content = f\"\"\"\n        <div style=\"font-family: Arial;\">\n            <h2>Veriphone Information</h2>\n            <p><strong>Phone Number:</strong> {data.get('phone', 'N/A')}</p>\n            <p><strong>Valid:</strong> {data.get('phone_valid', 'N/A')}</p>\n            <p><strong>Carrier:</strong> {carrier}</p>\n            <p><strong>Type:</strong> {data.get('phone_type', 'N/A')}</p>\n            <p><strong>Region:</strong> {phone_region}</p>\n            <p><strong>Country:</strong> {country}</p>\n            <p><strong>Country Code:</strong> {country_code}</p>\n            <p><strong>Country Prefix:</strong> {country_prefix}</p>\n            <p><strong>International Number:</strong> {international_number}</p>\n            <p><strong>Local Number:</strong> {local_number}</p>\n            <p><strong>E164 Format:</strong> {e164}</p>\n\n        </div>\n        \"\"\"\n        return html_content\n"
  },
  {
    "path": "modules/virus_total_tab.py",
    "content": "import io\nimport zipfile\nfrom datetime import date\nfrom time import time\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QAction, QIcon\nfrom PySide6.QtSvgWidgets import QSvgWidget\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QToolBar, QWidgetAction, QSizePolicy, QTextBrowser, QPushButton, \\\n    QHBoxLayout, QMessageBox\nfrom requests import post as requests_post\nfrom requests.exceptions import RequestException\n\n\nclass VirusTotal(QWidget):\n    def __init__(self):\n        super().__init__()\n        self.last_request_time = 0\n        self.requests_made_last_minute = 0\n        self.daily_requests_made = 0\n        self.current_date = date.today()\n        self.api_key = None\n        self.current_file_hash = None\n        self.current_file_content = None\n        self.current_file_name = None\n\n        self.init_ui()\n\n    def init_ui(self):\n        self.layout = QVBoxLayout(self)\n        self.layout.setSpacing(0)\n        self.layout.setContentsMargins(0, 0, 0, 0)\n\n        # First toolbar for the VirusTotal logo\n        self.logo_toolbar = QToolBar(self)\n        self.logo_toolbar.setContentsMargins(0, 0, 0, 0)  # Add some margins to the toolbar for better aesthetics\n\n        self.setup_logo_toolbar()\n        self.layout.addWidget(self.logo_toolbar)\n\n        self.action_toolbar = QToolBar(self)\n        self.action_toolbar.setContentsMargins(0, 0, 0, 0)\n        self.setup_action_toolbar()\n        self.layout.addWidget(self.action_toolbar)\n        self.action_toolbar.setVisible(False)  # Hide this toolbar initially\n\n        buttonLayout = QHBoxLayout()\n        # align the button vertically and horizontally\n        buttonLayout.setAlignment(Qt.AlignCenter)  # Align the buttons to the center\n\n        self.pass_hash_button = QPushButton(\"Pass Hash\")\n        self.pass_hash_button.clicked.connect(self.pass_hash)\n        self.pass_hash_button.setFixedSize(120, 40)  # Set fixed size for a modern look\n\n        buttonLayout.addWidget(self.pass_hash_button)\n\n        # Upload File Button\n        self.upload_file_button = QPushButton(\"Upload File\")\n        self.upload_file_button.clicked.connect(self.upload_file)\n        self.upload_file_button.setFixedSize(120, 40)  # Set fixed size for a modern look\n\n        buttonLayout.addWidget(self.upload_file_button)\n        self.layout.addLayout(buttonLayout)\n\n        self.info_text_edit = QTextBrowser(self)\n        self.info_text_edit.setReadOnly(True)\n        self.info_text_edit.setVisible(False)\n        self.layout.addWidget(self.info_text_edit)\n\n    def set_api_key(self, key):\n        self.api_key = key\n\n    def use_api_key(self):\n        if not self.api_key:\n            raise ValueError(\"API key not set\")\n\n    def spacer(self, policy1, policy2):\n        spacer = QWidget(self)\n        spacer.setSizePolicy(policy1, policy2)\n        return spacer\n\n    def setup_logo_toolbar(self):\n        self.logo_toolbar.addWidget(self.spacer(QSizePolicy.Expanding, QSizePolicy.Preferred))\n        self.virus_total_logo = QSvgWidget(\"Icons/VirusTotal_logo.svg\")\n        self.virus_total_logo.setFixedSize(141, 27)\n        logo_action = QWidgetAction(self)\n        logo_action.setDefaultWidget(self.virus_total_logo)\n        self.logo_toolbar.addAction(logo_action)\n        self.virus_total_logo.mousePressEvent = self.virus_total_website\n        self.virus_total_logo.setCursor(Qt.PointingHandCursor)\n\n    def setup_action_toolbar(self):\n        self.view_in_browser_action = QAction(QIcon('Icons/apps/internet-web-browser.svg'), \"View in Browser\",\n                                              self)\n        self.view_in_browser_action.triggered.connect(self.view_in_browser)\n        self.action_toolbar.addAction(self.view_in_browser_action)\n        self.view_in_browser_action.setVisible(True)\n\n        self.back_action = QAction(QIcon('Icons/icons8-left-arrow-50.png'), \"Back\", self)\n        self.back_action.triggered.connect(self.reset_ui)\n        self.action_toolbar.addAction(self.back_action)\n        self.action_toolbar.addWidget(self.spacer(QSizePolicy.Expanding, QSizePolicy.Preferred))\n\n        self.virus_total_logo = QSvgWidget(\"Icons/VirusTotal_logo.svg\")\n        self.virus_total_logo.setFixedSize(141, 27)\n        logo_action = QWidgetAction(self)\n        logo_action.setDefaultWidget(self.virus_total_logo)\n        self.action_toolbar.addAction(logo_action)\n        self.virus_total_logo.mousePressEvent = self.virus_total_website\n        self.virus_total_logo.setCursor(Qt.PointingHandCursor)\n\n    def virus_total_website(self, event):\n        import webbrowser\n        webbrowser.open(\"https://www.virustotal.com\")\n\n    def reset_ui(self):\n        self.info_text_edit.setVisible(False)\n        self.pass_hash_button.setVisible(True)\n        self.upload_file_button.setVisible(True)\n        self.action_toolbar.setVisible(False)  # Hide action toolbar\n\n    def set_file_hash(self, file_hash):\n        self.current_file_hash = file_hash\n\n    # set file content to expect file content as bytes and name as string\n    def set_file_content(self, file_content, file_name=\"unnamed_file\"):\n        \"\"\"Sets the current file content and assigns a default name if none is provided.\"\"\"\n        self.current_file_content = file_content\n        if not file_name:\n            self.current_file_name = \"unnamed_file\"\n        else:\n            self.current_file_name = file_name\n\n    def upload_file(self):\n        \"\"\"Prepares the file content and name for upload.\"\"\"\n        if not self.api_key:\n            QMessageBox.warning(self, \"API Key Not Set\",\n                                \"Please set the API key in the Options menu before uploading a file.\")\n            return\n\n        if self.current_file_content and self.current_file_name:\n            # Assuming current_file_content is the content of the file to upload,\n            # and current_file_name is the name of the file.\n            self.upload_file_to_virustotal(self.current_file_content, self.current_file_name)\n        else:\n            self.info_text_edit.setText(\"No file content or name provided.\")\n            self.info_text_edit.setVisible(True)\n\n    def zip_file_in_memory(self, content: bytes, file_name: str):\n        \"\"\"Creates a zip archive in memory containing the given file.\"\"\"\n        zip_buffer = io.BytesIO()\n        with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:\n            zip_file.writestr(file_name, content)\n        zip_buffer.seek(0)\n        return zip_buffer\n\n    def upload_file_to_virustotal(self, file_content, file_name):\n        \"\"\"Uploads a zipped file to VirusTotal.\"\"\"\n        # Here, file_content should be the content of the file to upload,\n        # and file_name should be the name of the file inside the zip.\n        zip_buffer = self.zip_file_in_memory(file_content, file_name)\n\n        url = \"https://www.virustotal.com/api/v3/files\"\n        headers = {\n            \"x-apikey\": self.api_key\n        }\n        files = {'file': (file_name + '.zip', zip_buffer.getvalue())}\n        response = requests_post(url, headers=headers, files=files)\n\n        if response.status_code == 200:\n            self.process_vt_response(response.json())\n        else:\n            print(\"Failed to upload file to VirusTotal:\", response.text)\n\n    def process_vt_response(self, response):\n        data = response.get('data', {})\n        file_id = data.get('id', 'N/A')\n        file_hash = data.get('attributes', {}).get('sha256', 'N/A')\n        upload_date = data.get('attributes', {}).get('date', 'N/A')\n        self.current_file_hash = file_hash\n        self.update_virustotal_info()\n        self.logo_toolbar.setVisible(False)\n        self.action_toolbar.setVisible(True)\n        self.view_in_browser_action.setVisible(False)\n\n    def pass_hash(self):\n        if not self.api_key:\n            QMessageBox.warning(self, \"API Key Not Set\",\n                                \"Please set the API key in the Options menu before passing a hash.\")\n            return\n\n        if not self.current_file_hash:\n            self.info_text_edit.setText(\"No hash provided.\")\n            self.info_text_edit.setVisible(True)\n            return\n        self.update_virustotal_info()\n        self.action_toolbar.setVisible(True)\n        self.view_in_browser_action.setVisible(True)\n        self.logo_toolbar.setVisible(False)\n\n    def update_virustotal_info(self):\n        self.info_text_edit.setVisible(True)\n        self.pass_hash_button.setVisible(False)\n        self.upload_file_button.setVisible(False)\n        if self.current_file_hash:\n            data = self.vt_getresult(self.current_file_hash)\n            if not data:  # Check if the data is empty. If empty, it means there was a rate limit error.\n                self.info_text_edit.setText(\"Failed to fetch data.\")\n                return\n            info_text = self.format_data_as_html(data)\n            self.info_text_edit.setHtml(info_text)\n\n    def vt_getresult(self, hashes):\n        # Check if we're on a new day\n        if date.today() != self.current_date:\n            self.current_date = date.today()\n            self.daily_requests_made = 0\n\n        # Check if we've exceeded daily limit\n        if self.daily_requests_made >= 500:\n            self.info_text_edit.setPlainText(\"Daily request limit exceeded. Please try again tomorrow.\")\n            return {}\n\n        # Check if we made a request in the last minute\n        current_time = time()\n        if current_time - self.last_request_time < 60:\n            self.requests_made_last_minute += 1\n            if self.requests_made_last_minute > 3:\n                # Inform the user about the rate limit with enhanced formatting\n                self.info_text_edit.setHtml(\n                    '<div style=\"text-align: center; padding: 20px;\">'\n                    '<p style=\"font-size: 20px; font-weight: bold;\">Rate Limit Exceeded</p>'\n                    '<p style=\"font-size: 16px;\">Please wait a minute and try again.</p>'\n                    '<p style=\"font-size: 16px;\">Or <a href=\"#\" style=\"color: blue; text-decoration: underline;\" '\n                    'onclick=\"viewInBrowser()\">view in browser</a>.</p>'\n                    '</div>'\n                )\n                return {}\n        else:\n            # If it's been more than a minute since the last request, reset the counter\n            self.requests_made_last_minute = 1\n\n        # Update the last request time and daily requests count\n        self.last_request_time = current_time\n        self.daily_requests_made += 1\n\n        headers = {\n            \"Accept-Encoding\": \"gzip, deflate\",\n            \"User-Agent\": \"gzip, My Python requests library example client or username\"\n        }\n        params = {'apikey': self.api_key, 'resource': hashes}\n        response = requests_post('https://www.virustotal.com/vtapi/v2/file/report', params=params, headers=headers)\n\n        # Handle the case where the response is not a valid JSON (for example, if the rate limit is exceeded)\n        try:\n            return response.json()\n        except RequestException:\n            self.info_text_edit.setPlainText(\"Error decoding JSON from the response. Please try again.\")\n            return {}\n\n    def format_data_as_html(self, data):\n        # Extract main details from the data\n        md5 = data.get('md5', 'N/A')\n        sha1 = data.get('sha1', 'N/A')\n        sha256 = data.get('sha256', 'N/A')\n        scan_date = data.get('scan_date', 'N/A')\n        positives = data.get('positives', 0)\n        total = data.get('total', 0)\n        permalink = data.get('permalink', 'N/A')\n\n        # Extract and format the scan results\n        scans = data.get('scans', {})\n        scan_rows = \"\"\n        for antivirus, result in scans.items():\n            detected = \"Yes\" if result.get('detected') else \"No\"\n            version = result.get('version', 'N/A')\n            last_update = result.get('update', 'N/A')\n            scan_result = result.get('result', 'N/A') or 'N/A'\n            scan_rows += f\"\"\"\n            <tr>\n                <td>{antivirus}</td>\n                <td>{detected}</td>\n                <td>{version}</td>\n                <td>{last_update}</td>\n                <td>{scan_result}</td>\n            </tr>\n            \"\"\"\n\n        # Create the HTML content\n        html_content = f\"\"\"\n        <div style=\"font-family: Arial;\">\n            <h2>VirusTotal Information</h2>\n            <p><strong>MD5:</strong> {md5}</p>\n            <p><strong>SHA1:</strong> {sha1}</p>\n            <p><strong>SHA256:</strong> {sha256}</p>\n            <p><strong>Last Scanned:</strong> {scan_date}</p>\n            <p><strong>Score:</strong> {positives}/{total}</p>\n            <p><strong>Permalink:</strong> <a href=\"{permalink}\">{permalink}</a></p>\n\n            <h3>Scan Results:</h3>\n            <table border=\"1\" cellpadding=\"5\">\n                <thead>\n                    <tr>\n                        <th>Antivirus</th>\n                        <th>Detected</th>\n                        <th>Version</th>\n                        <th>Last Update</th>\n                        <th>Result</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {scan_rows}\n                </tbody>\n            </table>\n        </div>\n        \"\"\"\n        return html_content\n\n    def view_in_browser(self):\n        \"\"\"Open the VirusTotal URL in the default web browser.\"\"\"\n        if self.current_file_hash:\n            import webbrowser\n            webbrowser.open(f\"https://www.virustotal.com/gui/file/{self.current_file_hash}/detection\")\n"
  },
  {
    "path": "requirements.txt",
    "content": "﻿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-normalizer==3.3.0\ncomtypes==1.2.0\ndocopt==0.6.2\ndocx==0.2.4\nenum-compat==0.0.3\net-xmlfile==1.1.0\nfiletype==1.2.0\nfrozenlist==1.4.0\ngreenlet==3.0.3\nhachoir==3.0a3\nHTMLParser==0.0.2\nhupper==1.12\nidna==3.4\nJinja2==3.1.2\nlibewf-python==20230212\nline-profiler==4.1.1\nline-profiler-pycharm==1.1.0\nlxml==5.1.0\nMarkupSafe==2.1.3\nmultidict==6.0.4\nnumpy==1.26.3\nolefile==0.46\nopencv-python==4.10.0.84\nopenpyxl==3.1.2\npandas==2.2.0\nPasteDeploy==3.0.1\nPillow==10.0.0\nplaster==1.1.2\nplaster-pastedeploy==1.0.1\nplayer==0.6.1\npsutil==5.9.5\npy-ewf-mount==1.0.0\npycaw==20230407\npycparser==2.21\nPyMuPDF==1.23.21\nPyMuPDFb==1.23.9\npypdf==4.0.1\nPyPDF2==3.0.1\npyramid==2.0.2\npyramid-jinja2==2.10\nPySide6==6.5.2\nPySide6-Addons==6.5.2\nPySide6-Essentials==6.5.2\npython-dateutil==2.8.2\npython-docx==1.1.0\npython-magic-bin==0.4.14\npython-pptx==0.6.23\npython-registry==1.3.1\npython-vlc==3.0.18122\npytsk3==20210801\npytz==2023.3\nrequests==2.31.0\nshiboken6==6.5.2\nsix==1.16.0\ntexttable==1.6.7\ntoml==0.10.2\ntranslationstring==1.4\ntyping_extensions==4.9.0\ntzdata==2023.4\nunicodecsv==0.14.1\nurllib3==2.0.5\nvenusian==3.0.0\nvulture==2.9.1\nWebOb==1.8.7\nxlrd==2.0.1\nXlsxWriter==3.1.9\nyarg==0.1.9\nyarl==1.9.2\nmoviepy~=1.0.3\npdf2image~=1.17.0\n"
  },
  {
    "path": "requirements_macos_silicon.txt",
    "content": "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.0\ncomtypes==1.2.0\ndocopt==0.6.2\ndocx==0.2.4\nenum-compat==0.0.3\net-xmlfile==1.1.0\nfiletype==1.2.0\nfrozenlist==1.4.0\ngreenlet==3.0.3\nhachoir==3.0a3\nHTMLParser==0.0.2\nhupper==1.12\nidna==3.4\nJinja2==3.1.2\nline-profiler==4.1.1\nline-profiler-pycharm==1.1.0\nlxml==5.1.0\nMarkupSafe==2.1.3\nmultidict==6.0.4\nolefile==0.46\nopenpyxl==3.1.2\npandas==2.2.0\nPasteDeploy==3.0.1\nplaster==1.1.2\nplaster-pastedeploy==1.0.1\nplayer==0.6.1\npsutil==5.9.5\npycaw==20230407\npycparser==2.21\npyramid==2.0.2\npyramid-jinja2==2.10\npython-dateutil==2.8.2\npython-docx==1.1.0\npython-pptx==0.6.23\npython-vlc==3.0.18122\nrequests==2.31.0\nsix==1.16.0\ntexttable==1.6.7\ntoml==0.10.2\ntranslationstring==1.4\ntyping_extensions==4.9.0\ntzdata==2023.4\nunicodecsv==0.14.1\nurllib3==2.0.5\nvenusian==3.0.0\nvulture==2.9.1\nWebOb==1.8.7\nxlrd==2.0.1\nXlsxWriter==3.1.9\nyarg==0.1.9\nyarl==1.9.2\nnumpy==1.26.3\nchardet==5.2.0\npython-magic==0.4.27\npdf2image==1.17.0\nmoviepy==1.0.3\nPyPDF2==3.0.1\nPyMuPDF==1.24.10\nPyMuPDFb==1.24.10\nPillow==10.4.0\npytsk3==20231007\nlibewf-python==20240506\npython-registry==1.3.1\nPySide6==6.5.2\nPySide6-Addons==6.5.2\nPySide6-Essentials==6.5.2\nopencv-python==4.10.0.84\n"
  },
  {
    "path": "styles/dark_theme.qss",
    "content": "/* Global Widget Styles */\nQWidget {\n    font-size: 14px;\n    color: #E0E0E0;  /* Light text color */\n    background-color: #2E2E2E;  /* Dark background color */\n}\n\n/* Button Styles */\nQPushButton {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    padding: 5px 15px;\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n    margin-left: 8px;\n    margin-right: 8px;\n}\nQPushButton:hover {\n    background-color: #4C4C4C;\n}\nQPushButton:pressed {\n    background-color: #5C5C5C;\n}\n\n/* CheckBox Styles */\nQCheckBox {\n    spacing: 5px;\n    color: #E0E0E0;\n}\nQCheckBox::indicator {\n    width: 13px;\n    height: 13px;\n    border-radius: 6px;\n}\nQCheckBox::indicator:unchecked {\n    border: 1px solid #5A5A5A;\n    background-color: #3C3C3C;\n}\nQCheckBox::indicator:checked {\n    image: url('Icons/icons8-tick-48.png');\n}\n\n/* ComboBox Styles */\nQComboBox {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    padding: 5px 10px;\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n    selection-background-color: #1E90FF;  /* A shade of blue */\n}\nQComboBox::drop-down {\n    subcontrol-origin: padding;\n    subcontrol-position: top right;\n    width: 25px;\n    border-left-width: 1px;\n    border-left-color: #5A5A5A;\n    border-left-style: solid;\n    border-top-right-radius: 4px;\n    border-bottom-right-radius: 4px;\n    background-color: #4C4C4C;\n}\nQComboBox::down-arrow {\n    image: url('Icons/icons8-dropdown-48.png');\n    width: 16px;\n    height: 16px;\n}\nQComboBox:hover {\n    border: 1px solid #A2A9B1;\n}\nQComboBox::drop-down:hover {\n    background-color: #4C4C4C;\n}\n\n/* Dock Widget Styles */\nQDockWidget {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    background-color: #2E2E2E;\n}\nQDockWidget::title {\n    background: #3C3C3C;\n    padding: 1px;\n    border-top-left-radius: 2px;\n    border-top-right-radius: 2px;\n}\nQDockWidget::close-button, QDockWidget::float-button {\n    border: 1px solid transparent;\n    border-radius: 5px;\n    background: #3C3C3C;\n}\nQDockWidget::close-button:hover, QDockWidget::float-button:hover {\n    background: #4C4C4C;\n}\nQDockWidget::close-button:pressed, QDockWidget::float-button:pressed {\n    background: #5C5C5C;\n}\n\n/* List and Table Widget Styles */\nQListWidget, QTableWidget {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    background-color: #2E2E2E;\n    color: #E0E0E0;\n}\nQListWidget::item, QTableWidget::item {\n    padding: 5px;\n}\nQListWidget::item:selected, QTableWidget::item:selected {\n    background-color: #4C4C4C;\n    color: #E0E0E0;\n}\n\n/* Menu and MenuBar Styles */\nQMenuBar {\n    background-color: #3C3C3C;\n    border-bottom: 1px solid #5A5A5A;\n}\nQMenuBar::item {\n    padding: 5px 8px;\n    border-radius: 2px;\n    color: #E0E0E0;\n}\nQMenuBar::item:selected {\n    background-color: #4C4C4C;\n}\nQMenuBar::item:pressed {\n    background-color: #5C5C5C;\n}\n\n/* MessageBox Styles */\nQMessageBox {\n    background-color: #2E2E2E;\n    border: 1px solid #5A5A5A;\n    color: #E0E0E0;\n}\n\n/* ScrollBar Styles */\nQScrollBar:vertical {\n    border: none;\n    background: #3C3C3C;\n    width: 10px;\n    margin: 0px;\n    border-radius: 0px;\n}\nQScrollBar::handle:vertical {\n    background: #5A5A5A;\n    min-height: 20px;\n    border-radius: 5px;\n}\nQScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {\n    border: none;\n    background: none;\n    height: 0px;\n}\n\nQScrollBar:horizontal {\n    border: none;\n    background: #3C3C3C;\n    height: 10px;\n    margin: 0px;\n    border-radius: 0px;\n}\nQScrollBar::handle:horizontal {\n    background: #5A5A5A;\n    min-width: 20px;\n    border-radius: 5px;\n}\nQScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {\n    border: none;\n    background: none;\n    width: 0px;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\nQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical,\nQScrollBar::left-arrow:horizontal, QScrollBar::right-arrow:horizontal {\n    background: none;\n}\n\n/* Slider Styles */\nQSlider::groove:horizontal {\n    border: 1px solid #5A5A5A;\n    height: 8px;\n    background: #3C3C3C;\n    margin: 2px 0;\n    border-radius: 4px;\n}\nQSlider::handle:horizontal {\n    background: #5A5A5A;\n    border: 1px solid #5A5A5A;\n    width: 14px;\n    margin: -2px 0;\n    border-radius: 7px;\n}\nQSlider::handle:horizontal:hover {\n    background: #6A6A6A;\n}\nQSlider::handle:horizontal:pressed {\n    background: #7A7A7A;\n}\nQSlider::add-page:horizontal {\n    background: #4C4C4C;\n    border: 1px solid #5A5A5A;\n    height: 8px;\n    border-radius: 4px;\n}\n\n/* TabWidget and TabBar Styles */\nQTabWidget {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    background-color: #2E2E2E;\n}\n\nQTabWidget::pane {\n    border-top: 1px solid #5A5A5A;  /* Darker gray color to match dark mode */\n}\n\nQTabBar::tab {\n    background: #3C3C3C;\n    border: 1px solid #5A5A5A;\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n    min-width: 8ex;\n    padding: 2px;\n    color: #E0E0E0;\n}\nQTabBar::tab:selected, QTabBar::tab:hover {\n    background: #2E2E2E;\n}\nQTabBar::tab:selected {\n    border-color: #9B9B9B;\n    border-bottom-color: #C2C7CB; /* Separator effect */\n}\nQTabBar::tab:!selected {\n    margin-top: 2px; /* Unselected tabs are slightly raised */\n}\n\n\n\n/* ToolBar Styles */\nQToolBar {\n    background-color: #2E2E2E;\n    border-bottom: 1px solid #5A5A5A;\n    padding: 5px;\n}\nQToolBar::item:hover {\n    background-color: #4C4C4C;\n}\nQToolBar::item:pressed {\n    background-color: #5C5C5C;\n}\n\n\n\n/* Other Widget Styles */\nQLineEdit, QTextEdit, QAudioWidget {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n}\nQLineEdit:hover, QTextEdit:hover {\n    border: 1px solid #A2A9B1;\n}\nQLineEdit:focus {\n    border: 1px solid #1E90FF;  /* Blue border on focus */\n}\n\nQTreeWidget {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    background-color: #2E2E2E;\n    color: #E0E0E0;\n}\n\nQTreeWidget::item:selected {\n    background-color: #505050;\n    color: #E0E0E0;\n}\n\n\n\n/* Action Styles */\nQAction {\n    padding: 5px 10px;\n    border: 1px solid #5A5A5A;\n    border-radius: 4px;\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n}\nQAction:hover {\n    background-color: #4C4C4C;\n    border: 1px solid #A2A9B1;\n}\nQAction:pressed {\n    background-color: #5C5C5C;\n    border: 1px solid #1E90FF;\n}\n\nQGroupBox {\n    border-radius: 4px;\n    color: #E0E0E0;\n}\n\n/* Additional Styles for Consistency */\nQHeaderView::section {\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n    padding: 5px;\n    border-style: none;\n    border-bottom: 1px solid #5A5A5A;\n    border-right: 1px solid #5A5A5A;\n}\nQHeaderView::section:horizontal {\n    border-top: 1px solid #5A5A5A;\n}\nQHeaderView::section:vertical {\n    border-left: 1px solid #5A5A5A;\n}\n\n/* Highlighting Selected Items */\nQTableWidget::item:selected, QListWidget::item:selected {\n    background-color: #505050;\n    color: #E0E0E0;\n}\n\n\n#exportButton {\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    padding: 5px 30px 5px 5px;  /* Adjust right padding to push text more to the left */\n    background-color: #3C3C3C;  /* Dark button background */\n    color: #E0E0E0;  /* Light text color */\n    width: 60px;  /* Adjust width as necessary to fit text and menu arrow */\n}\n\n#exportButton::menu-button {\n    subcontrol-origin: padding;\n    subcontrol-position: top right;\n    width: 25px;\n    border-left-width: 1px;\n    border-left-color: #5A5A5A;\n    border-left-style: solid;\n    border-top-right-radius: 2px;\n    border-bottom-right-radius: 2px;\n    background-color: #4C4C4C;  /* Slightly lighter background for menu button */\n}\n\n#exportButton::menu-button:hover {\n    background-color: #6A6A6A;  /* Lighter shade on hover */\n}\n\n#exportButton::menu-arrow {\n    image: url('Icons/icons8-dropdown-48.png');\n    width: 16px;  /* Adjust the width of the image */\n    height: 16px;  /* Adjust the height of the image */\n}\n\nQLabel {\n    background-color: transparent;  /* Set label background to transparent */\n}\n\n\n/* Styles specifically for listingTable QTableWidget in dark theme */\n#listingTable {\n    gridline-color: #5A5A5A;\n    font-size: 12px;\n    background-color: #2E2E2E;\n    color: #E0E0E0;  /* Light text color for dark theme */\n}\n\n#listingTable::item {\n    padding: 5px;\n    color: #E0E0E0;  /* Light text color */\n    background-color: #3C3C3C;  /* Darker background for table rows */\n}\n\n#listingTable::item:selected {\n    background-color: #505050;  /* Highlight selected items with a contrasting color */\n}\n\n#listingTable QHeaderView::section {\n    background-color: #4C4C4C;  /* Dark header background */\n    color: #E0E0E0;  /* Light text color */\n    padding: 5px;\n    border-style: none;\n    border-bottom: 1px solid #3C3C3C;\n    border-right: 1px solid #3C3C3C;\n}\n\n#listingTable QHeaderView::section:horizontal {\n    border-top: 1px solid #3C3C3C;\n    margin-top: 0px;\n    padding-top: 2px;\n}\n\n#listingTable QHeaderView::section:vertical {\n    border-left: 1px solid #3C3C3C;\n}\n\n/* QMenu Styles */\nQMenu {\n    background-color: #3C3C3C;  /* Dark background for the menu */\n    border: 1px solid #5A5A5A;  /* Border for the menu */\n    border-radius: 6px;  /* Rounded corners for modern look */\n    padding: 4px;  /* Padding around menu */\n    color: #E0E0E0;  /* Text color for the menu */\n}\n\n/* QMenu item default style */\nQMenu::item {\n    padding: 5px 25px 5px 10px;  /* Padding for menu items */\n    border-radius: 4px;  /* Rounded corners for menu items */\n    background-color: transparent;  /* Transparent background */\n    color: #E0E0E0;  /* Default text color */\n}\n\n/* QMenu item hover style */\nQMenu::item:hover {\n    background-color: #505050;  /* Background color when hovered */\n    color: #FFFFFF;  /* Text color when hovered */\n}\n\n/* QMenu item selected style */\nQMenu::item:selected {\n    background-color: #4C4C4C;  /* Background color when selected */\n    color: #FFFFFF;  /* Text color when selected */\n}\n\n/* QMenu separator style */\nQMenu::separator {\n    height: 1px;\n    background-color: #5A5A5A;  /* Separator color */\n    margin: 3px 5px;  /* Spacing around separator */\n}\n\n\n\n\n/* General styles for all QTableWidgets in dark theme */\nQTableWidget, QTreeWidget, QListWidget {\n    gridline-color: #5A5A5A;  /* Gridline color for dark mode */\n    font-size: 12px;\n    background-color: #2E2E2E;  /* Dark background for tables */\n    color: #E0E0E0;  /* Light text color */\n}\n\n/* Table items */\nQTableWidget::item, QTreeWidget::item, QListWidget::item {\n    padding: 5px;\n    color: #E0E0E0;  /* Light text color */\n    background-color: #3C3C3C;  /* Darker background for table rows */\n}\n\nQTableWidget::item:selected, QTreeWidget::item:selected, QListWidget::item:selected {\n    background-color: #505050;  /* Highlight selected items with a contrasting color */\n}\n\n/* Header styles for all tables and trees */\nQHeaderView::section {\n    background-color: #4C4C4C;  /* Dark header background */\n    color: #E0E0E0;  /* Light text color */\n    padding: 5px;\n    border-style: none;\n    border-bottom: 1px solid #3C3C3C;\n    border-right: 1px solid #3C3C3C;\n}\n\nQHeaderView::section:horizontal {\n    border-top: 1px solid #3C3C3C;\n}\n\nQHeaderView::section:vertical {\n    border-left: 1px solid #3C3C3C;\n}\n\n/* Set the alternating row colors for better visibility */\nQTableWidget, QTreeWidget, QListWidget {\n    alternate-background-color: #383838;  /* Dark gray for alternating rows */\n}\n\n/* Vertical header (row numbers) */\nQHeaderView::section:vertical {\n    background-color: #2E2E2E;  /* Same color as table background */\n    color: #E0E0E0;  /* Light text color */\n}\n\n/* Styles for scrollbars in dark theme */\nQScrollBar:vertical, QScrollBar:horizontal {\n    background: #3C3C3C;  /* Darker background for scrollbars */\n    width: 10px;\n    height: 10px;\n}\n\nQScrollBar::handle:vertical, QScrollBar::handle:horizontal {\n    background: #5A5A5A;  /* Scroll handle color */\n    border-radius: 5px;\n}\n\nQScrollBar::add-line:vertical, QScrollBar::sub-line:vertical,\nQScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {\n    background: none;\n    height: 0px;\n    width: 0px;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\n\n#softwareInfoLabel {\n    font-size: 16pt;             /* Slightly larger font for the main title */\n    font-weight: bold;\n    color: #ffffff;              /* White text for better contrast on dark background */\n    padding: 10px 0;             /* Add some vertical padding */\n}\n\n#subtitleLabel {\n    font-size: 12pt;\n    color: #c7c7c7;              /* Lighter gray for the subtitle */\n    padding: 5px 0;              /* Add a little space around the subtitle */\n}\n\n/* Search Results Area Styling for Dark Theme */\n#search_results_frame {\n    background-color: #2E2E2E;\n    border: 1px solid #5A5A5A;\n    border-radius: 2px;\n    padding: 1px;\n}\n\n#search_results_title {\n    background-color: #4C4C4C;\n    color: #E0E0E0;\n    padding: 2px;\n    border-bottom: 1px solid #5A5A5A;\n}\n\n#search_results_widget {\n    background-color: #2E2E2E;\n    color: #E0E0E0;\n    border: none;\n    font-size: 10px;\n}\n\n#search_results_widget::item {\n    padding: 2px;\n}\n\n#search_results_widget::item:selected {\n    background-color: #505050;\n}\n\n/* UnifiedViewer Placeholder Label - \"No content loaded\" message */\n#placeholderLabel {\n    background-color: #3C3C3C;  /* Dark gray background */\n    font-size: 16px;\n    color: #888888;  /* Medium gray text */\n    padding: 10px;\n}\n\n/* AudioVideoPlayer Audio-Only Label - \"Playing Audio\" message */\n#audioOnlyLabel {\n    background-color: #4C4C4C;  /* Slightly lighter dark gray */\n    font-size: 18px;\n    color: #B0B0B0;  /* Light gray text */\n    padding: 20px;\n}\n\n/* Toolbar Compact Styles - for PictureViewer and PDFViewer */\nQToolBar {\n    spacing: 2px;\n    padding: 1px;\n}\n\nQToolButton {\n    padding: 2px;\n    margin: 1px;\n}\n\n/* Media Player Sliders - Position and Volume */\nQSlider::groove:horizontal {\n    height: 5px;\n    margin: 2px 0;\n}\n\nQSlider::handle:horizontal {\n    width: 10px;\n    margin: -3px 0;\n}\n\n/* Search Toolbar Styles (Listing View) */\nQLineEdit#listingSearchBar {\n    border: 1px solid #5A5A5A;\n    border-radius: 4px;\n    padding: 6px 10px;\n    background-color: #3C3C3C;\n    color: #E0E0E0;\n    font-size: 13px;\n}\n\nQLineEdit#listingSearchBar:hover {\n    border: 1px solid #A2A9B1;\n}\n\nQLineEdit#listingSearchBar:focus {\n    border: 1px solid #1E90FF;\n}\n\nQToolButton#clearSearchBtn {\n    border: 1px solid #5A5A5A;\n    border-radius: 4px;\n    background-color: #3C3C3C;\n    padding: 4px;\n}\n\nQToolButton#clearSearchBtn:hover {\n    background-color: #4C4C4C;\n    border: 1px solid #A2A9B1;\n}\n\nQToolButton#clearSearchBtn:pressed {\n    background-color: #5C5C5C;\n}\n\n/* File Type Filter GroupBox */\nQGroupBox#fileTypeGroup {\n    border: none;\n    background-color: transparent;\n    padding: 0px;\n    margin: 0px;\n}\n\nQGroupBox#fileTypeGroup QCheckBox {\n    spacing: 6px;\n    font-size: 12px;\n    padding: 2px;\n}\n\nQGroupBox#fileTypeGroup QCheckBox:hover {\n    color: #FFFFFF;\n}\n\n"
  },
  {
    "path": "styles/light_theme.qss",
    "content": "/* Global Widget Styles */\nQWidget {\n    font-size: 14px;\n    color: #333333;\n    background-color: #FFFFFF;\n}\n\n/* Button Styles */\nQPushButton {\n    border: 1px solid #ced4da;\n    border-radius: 2px;\n    padding: 5px 15px;\n    background-color: #ffffff;\n    margin-left: 8px;\n    margin-right: 8px;\n}\nQPushButton:hover {\n    background-color: #e7e7e7;\n}\nQPushButton:pressed {\n    background-color: #d7d7d7;\n}\n\n/* CheckBox Styles */\nQCheckBox {\n    spacing: 5px;\n}\nQCheckBox::indicator {\n    width: 13px;\n    height: 13px;\n    border-radius: 6px;\n}\nQCheckBox::indicator:unchecked {\n    border: 1px solid #d7d7d7;\n    background-color: #ffffff;\n}\nQCheckBox::indicator:checked {\n    image: url('Icons/icons8-tick-48.png');\n}\n\n/* ComboBox Styles */\nQComboBox {\n    border: 1px solid #ced4da;\n    border-radius: 2px;\n    padding: 5px 10px;\n    background-color: #ffffff;\n    selection-background-color: #56CCF2;\n}\nQComboBox::drop-down {\n    subcontrol-origin: padding;\n    subcontrol-position: top right;\n    width: 25px;\n    border-left-width: 1px;\n    border-left-color: #ced4da;\n    border-left-style: solid;\n    border-top-right-radius: 4px;\n    border-bottom-right-radius: 4px;\n}\nQComboBox::down-arrow {\n    image: url('Icons/icons8-dropdown-48.png');\n    width: 16px;\n    height: 16px;\n}\nQComboBox:hover {\n    border: 1px solid #a2a9b1;\n}\nQComboBox::drop-down:hover {\n    background-color: #f5f5f5;\n}\n\n/* Dock Widget Styles */\nQDockWidget {\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    background-color: #ffffff;\n}\nQDockWidget::title {\n    background: #f5f5f5;\n    padding: 1px;\n    border-top-left-radius: 2px;\n    border-top-right-radius: 2px;\n\n}\nQDockWidget::close-button, QDockWidget::float-button {\n    border: 1px solid transparent;\n    border-radius: 5px;\n    background: #f5f5f5;\n}\nQDockWidget::close-button:hover, QDockWidget::float-button:hover {\n    background: #e7e7e7;\n}\nQDockWidget::close-button:pressed, QDockWidget::float-button:pressed {\n    background: #d7d7d7;\n}\n\n/* Frame Styles */\nQFrame {\n    background-color: #ffffff;\n    border: none; /* Remove border from all QFrames by default */\n}\n\n/* Only apply borders to specific frames where needed */\nQFrame#search_results_frame, QDockWidget > QFrame {\n    border: 1px solid #E5E5E5;\n}\n\n/* List and Table Widget Styles */\nQListWidget, QTableWidget {\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    background-color: #ffffff;\n}\nQListWidget::item, QTableWidget::item {\n    padding: 5px;\n}\nQListWidget::item:selected, QTableWidget::item:selected {\n    background-color: #f2f2f2;\n    color: #333333;\n}\n\n\n/* Menu and MenuBar Styles */\nQMenuBar {\n    background-color: #FFFFFF;\n    border-bottom: 1px solid #E5E5E5;\n}\nQMenuBar::item {\n    padding: 5px 8px;\n    border-radius: 2px;\n}\nQMenuBar::item:selected {\n    background-color: #e7e7e7;\n}\nQMenuBar::item:pressed {\n    background-color: #d7d7d7;\n}\n\nQMenu {\n    background-color: #FFFFFF;\n    border: 1px solid #C0C0C0;\n    border-radius: 6px;\n    padding: 4px;\n}\nQMenu::item {\n    padding: 5px 25px 5px 10px;\n    border-radius: 4px;\n    color: #333333;\n}\nQMenu::item:selected {\n    background-color: #e7e7e7;\n}\nQMenu::item:pressed {\n    background-color: #d7d7d7;\n}\nQMenu::separator {\n    height: 1px;\n    background-color: #E5E5E5;\n    margin: 3px 5px;\n}\n\n/* MessageBox Styles */\nQMessageBox {\n    background-color: #ffffff;\n    border: 1px solid #E5E5E5;\n}\n\nQScrollBar:vertical {\n    border: none;\n    background: #F8F8F8;\n    width: 10px;\n    margin: 0px;\n    border-radius: 0px;\n}\n\nQScrollBar::handle:vertical {\n    background: #C1C1C1;\n    min-height: 20px;\n    border-radius: 5px;\n}\n\nQScrollBar::add-line:vertical {\n    border: none;\n    background: none;\n    height: 0px;\n    subcontrol-position: bottom;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:vertical {\n    border: none;\n    background: none;\n    height: 0px;\n    subcontrol-position: top;\n    subcontrol-origin: margin;\n}\n\nQScrollBar:horizontal {\n    border: none;\n    background: #F8F8F8;\n    height: 10px;\n    margin: 0px;\n    border-radius: 0px;\n}\n\nQScrollBar::handle:horizontal {\n    background: #C1C1C1;\n    min-width: 20px;\n    border-radius: 5px;\n}\n\nQScrollBar::add-line:horizontal {\n    border: none;\n    background: none;\n    width: 0px;\n    subcontrol-position: right;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal {\n    border: none;\n    background: none;\n    width: 0px;\n    subcontrol-position: left;\n    subcontrol-origin: margin;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\nQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical,\nQScrollBar::left-arrow:horizontal, QScrollBar::right-arrow:horizontal {\n    background: none;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {\n    background: none;\n}\n\n\n\n/* Slider Styles */\nQSlider::groove:horizontal {\n    border: 1px solid #E5E5E5;\n    height: 8px;\n    background: #ffffff;\n    margin: 2px 0;\n    border-radius: 4px;\n}\nQSlider::handle:horizontal {\n    background: #ffffff;\n    border: 1px solid #E5E5E5;\n    width: 14px;\n    margin: -2px 0;\n    border-radius: 7px;\n}\nQSlider::handle:horizontal:hover {\n    background: #b7b7b7;\n}\nQSlider::handle:horizontal:pressed {\n    background: #c7c7c7;\n}\nQSlider::add-page:horizontal {\n    background: #e7e7e7;\n    border: 1px solid #E5E5E5;\n    height: 8px;\n    border-radius: 4px;\n}\n\n/* TabWidget and TabBar Styles */\nQTabWidget {\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    background-color: #ffffff;\n}\n\nQTabWidget::pane {\n    border-top: 1px solid #E5E5E5;\n}\n\nQTabBar {\n    background-color: #FFFFFF;\n}\n\nQTabBar::tab {\n    background: #F5F5F5;\n    border: 1px solid #E5E5E5;\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n    min-width: 8ex;\n    padding: 5px;\n    color: #333333;\n}\n\nQTabBar::tab:selected, QTabBar::tab:hover {\n    background: #FFFFFF;\n}\n\nQTabBar::tab:selected {\n    border-color: #C2C7CB;\n    border-bottom-color: #FFFFFF; /* Same as tab background for seamless transition */\n}\n\nQTabBar::tab:!selected {\n    margin-top: 2px; /* Unselected tabs are slightly raised */\n}\n\n/* ToolBar Styles */\nQToolBar {\n    background-color: #FFFFFF;\n    border-bottom: 1px solid #E5E5E5;\n    padding: 5px;\n}\nQToolBar::item:hover {\n    background-color: #e7e7e7;\n}\nQToolBar::item:pressed {\n    background-color: #d7d7d7;\n}\n\n/* Other Widget Styles */\nQLineEdit, QTextEdit, QAudioWidget {\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    background-color: #ffffff;\n}\nQLineEdit:hover, QTextEdit:hover {\n    border: 1px solid #a2a9b1;\n}\nQLineEdit:focus {\n    border: 1px solid #56CCF2;\n}\n\nQTreeWidget {\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    background-color: #ffffff;\n}\n\n\n/* Action Styles */\nQAction {\n    padding: px 10px;\n    border: 1px solid #ced4da;\n    border-radius: 4px;\n    background-color: #ffffff;\n}\nQAction:hover {\n    background-color: #f5f5f5;\n    border: 1px solid #a2a9b1;\n}\nQAction:pressed {\n    background-color: #e7e7e7;\n    border: 1px solid #56CCF2;\n}\n\nQGroupBox {\n    border-radius: 4px;\n    border: 1px solid #E5E5E5;\n    background-color: #FFFFFF;\n}\n\n\n#exportButton {\n    border: 1px solid #ced4da;\n    border-radius: 2px;\n    padding: 5px 30px 5px 5px; /* Adjust right padding to push text more to the left */\n    background-color: #ffffff;\n    width: 60px; /* Adjust width as necessary to fit text and menu arrow */\n}\n\n#exportButton::menu-button {\n    subcontrol-origin: padding;\n    subcontrol-position: top right;\n    width: 25px;\n    border-left-width: 1px;\n    border-left-color: #ced4da;\n    border-left-style: solid;\n    border-top-right-radius: 2px;\n    border-bottom-right-radius: 2px;\n}\n\n#exportButton::menu-button:hover {\n    background-color: #ced4da;\n}\n\n#exportButton::menu-arrow {\n    image: url('Icons/icons8-dropdown-48.png');\n    width: 16px;  /* Adjust the width of the image */\n    height: 16px;  /* Adjust the height of the image */\n}\n\n\n\n\n/* General styles for all QTableWidgets, QTreeWidgets, and QListWidgets in light theme */\nQTableWidget, QTreeWidget, QListWidget {\n    gridline-color: #E5E5E5;  /* Light gridline color */\n    font-size: 12px;\n    background-color: #FFFFFF;  /* Light background for tables */\n    color: #000000;  /* Dark text color */\n}\n\n/* Table items */\nQTableWidget::item, QTreeWidget::item, QListWidget::item {\n    padding: 5px;\n    color: #000000;  /* Dark text color */\n    background-color: #FFFFFF;  /* White background for table rows */\n}\n\nQTableWidget::item:selected, QTreeWidget::item:selected, QListWidget::item:selected {\n    background-color: #CCE8FF;  /* Light blue highlight for selected items */\n}\n\n/* Header styles for all tables and trees */\nQHeaderView::section {\n    background-color: #F5F5F5;  /* Light gray header background */\n    color: #000000;  /* Dark text color */\n    padding: 5px;\n    border-style: none;\n    border-bottom: 1px solid #E5E5E5;\n    border-right: 1px solid #E5E5E5;\n}\n\nQHeaderView::section:horizontal {\n    border-top: 1px solid #E5E5E5;\n}\n\n/* Specific styles for listingTable's header to remove extra space */\n#listingTable QHeaderView::section:horizontal {\n    border-top: 1px solid #E5E5E5;\n    margin-top: 0px;\n    padding-top: 2px;\n}\n\nQHeaderView::section:vertical {\n    border-left: 1px solid #E5E5E5;\n}\n\n/* Set the alternating row colors for better visibility */\nQTableWidget, QTreeWidget, QListWidget {\n    alternate-background-color: #F8F8F8;  /* Very light gray for alternating rows */\n}\n\n/* Vertical header (row numbers) */\nQHeaderView::section:vertical {\n    background-color: #F5F5F5;  /* Same color as table background */\n    color: #000000;  /* Dark text color */\n}\n\n/* Styles for scrollbars in light theme */\nQScrollBar:vertical, QScrollBar:horizontal {\n    background: #F0F0F0;  /* Light background for scrollbars */\n    width: 10px;\n    height: 10px;\n}\n\nQScrollBar::handle:vertical, QScrollBar::handle:horizontal {\n    background: #C0C0C0;  /* Scroll handle color */\n    border-radius: 5px;\n}\n\nQScrollBar::add-line:vertical, QScrollBar::sub-line:vertical,\nQScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {\n    background: none;\n    height: 0px;\n    width: 0px;\n}\n\nQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,\nQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {\n    background: none;\n}\n\n/* Vertical header style for row numbers */\nQHeaderView::section:vertical {\n    background-color: #F5F5F5;  /* Match the table background */\n}\n\n/* Styles specifically for lists and tree items in light theme */\nQListWidget, QTreeWidget::item {\n    background-color: #FFFFFF;  /* White background for rows */\n    color: #000000;  /* Dark text color */\n}\n\nQListWidget::item:selected, QTreeWidget::item:selected {\n    background-color: #CCE8FF;  /* Light blue highlight for selected items */\n}\n\n#softwareInfoLabel {\n    font-size: 16pt;             /* Slightly larger font for the main title */\n    font-weight: bold;\n    color: #333333;              /* Dark text color */\n    padding: 10px 0;             /* Add some vertical padding */\n}\n\n#subtitleLabel {\n    font-size: 12pt;\n    color: #666666;              /* Medium gray for the subtitle */\n    padding: 5px 0;              /* Add a little space around the subtitle */\n}\n\n/* Search Results Area Styling for Light Theme */\n#search_results_frame {\n    background-color: #FFFFFF;\n    border: 1px solid #E5E5E5;\n    border-radius: 2px;\n    padding: 1px;\n}\n\n#search_results_title {\n    background-color: #F0F0F0;\n    color: #333333;\n    padding: 2px;\n    border-bottom: 1px solid #D3D3D3;\n}\n\n#search_results_widget {\n    background-color: #FFFFFF;\n    color: #333333;\n    border: none;\n    font-size: 10px;\n}\n\n#search_results_widget::item {\n    padding: 2px;\n}\n\n#search_results_widget::item:selected {\n    background-color: #CCE8FF;\n}\n\n/* Address bar in hex view */\nQLabel[text=\"Address\"] {\n    background-color: #F5F5F5;\n    color: #333333;\n}\n\nQLabel[text=\"00\"], QLabel[text=\"01\"], QLabel[text=\"02\"], QLabel[text=\"03\"],\nQLabel[text=\"04\"], QLabel[text=\"05\"], QLabel[text=\"06\"], QLabel[text=\"07\"],\nQLabel[text=\"08\"], QLabel[text=\"09\"], QLabel[text=\"0A\"], QLabel[text=\"0B\"],\nQLabel[text=\"0C\"], QLabel[text=\"0D\"], QLabel[text=\"0E\"], QLabel[text=\"0F\"],\nQLabel[text=\"ASCII\"] {\n    background-color: #F5F5F5;\n    color: #333333;\n}\n\n/* UnifiedViewer Placeholder Label - \"No content loaded\" message */\n#placeholderLabel {\n    background-color: #F0F0F0;  /* Light gray background */\n    font-size: 16px;\n    color: #888888;  /* Medium gray text */\n    padding: 10px;\n}\n\n/* AudioVideoPlayer Audio-Only Label - \"Playing Audio\" message */\n#audioOnlyLabel {\n    background-color: #E0E0E0;  /* Light gray background */\n    font-size: 18px;\n    color: #444444;  /* Dark gray text */\n    padding: 20px;\n}\n\n/* Toolbar Compact Styles - for PictureViewer and PDFViewer */\nQToolBar {\n    spacing: 2px;\n    padding: 1px;\n}\n\nQToolButton {\n    padding: 2px;\n    margin: 1px;\n}\n\n/* Media Player Sliders - Position and Volume */\nQSlider::groove:horizontal {\n    height: 5px;\n    margin: 2px 0;\n}\n\nQSlider::handle:horizontal {\n    width: 10px;\n    margin: -3px 0;\n}\n\n/* Search Toolbar Styles (Listing View) */\nQLineEdit#listingSearchBar {\n    border: 1px solid #C0C0C0;\n    border-radius: 4px;\n    padding: 6px 10px;\n    background-color: #FFFFFF;\n    color: #333333;\n    font-size: 13px;\n}\n\nQLineEdit#listingSearchBar:hover {\n    border: 1px solid #999999;\n}\n\nQLineEdit#listingSearchBar:focus {\n    border: 1px solid #1E90FF;\n}\n\nQToolButton#clearSearchBtn {\n    border: 1px solid #C0C0C0;\n    border-radius: 4px;\n    background-color: #F5F5F5;\n    padding: 4px;\n}\n\nQToolButton#clearSearchBtn:hover {\n    background-color: #E7E7E7;\n    border: 1px solid #999999;\n}\n\nQToolButton#clearSearchBtn:pressed {\n    background-color: #D7D7D7;\n}\n\n/* File Type Filter GroupBox */\nQGroupBox#fileTypeGroup {\n    border: none;\n    background-color: transparent;\n    padding: 0px;\n    margin: 0px;\n}\n\nQGroupBox#fileTypeGroup QCheckBox {\n    spacing: 6px;\n    font-size: 12px;\n    padding: 2px;\n    color: #333333;\n}\n\nQGroupBox#fileTypeGroup QCheckBox:hover {\n    color: #000000;\n}\n"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/Arsenal Recon - End User License Agreement.txt",
    "content": "ARSENAL RECON END USER LICENSE AGREEMENT\n\nPLEASE READ THIS LICENSE AGREEMENT CAREFULLY BEFORE USING THIS SOFTWARE. BY INSTALLING AND/OR USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, UNDERSTAND IT, AND AGREE TO BE BOUND BY ITS TERMS AND CONDITIONS.\n\nArsenal Consulting, Inc. d/b/a Arsenal Recon (“Arsenal”) is willing to license the enclosed software (the “Software”) to You as the individual, the Company, or the Legal Entity that will be utilizing the Software (“You” or “Your”) only on the condition that You accept all of the terms of this End User License Agreement (“End User License Agreement” or “License Agreement”). This is a legal and enforceable contract between You and Arsenal. By opening this package, breaking the seal, selecting the “I accept the terms in the License Agreement” checkbox or otherwise indicating Your assent electronically, or loading the software, You agree to the terms and conditions of this License Agreement. If You do not agree to these terms and conditions, click the “Cancel” button or otherwise indicate Your refusal, make no further use of the Software, and contact Your vendor or Arsenal for information on how to obtain a refund of the money You paid for the Software (less shipping, handling, and any applicable taxes except in certain states and countries where shipping, handling and taxes are refundable) at any time during the sixty (60) day period following the date of purchase.\n\nEach copy of the Software is individually licensed for use on a single workstation and may be installed on only one workstation at a time. If you have purchased a software-protection dongle, the Software may be installed on as many workstations as You desire as long as it is only used on the workstation that the dongle is currently plugged into.\n\nCopyright & Proprietary Information. The Software (including any accompanying features and services) and documentation (including any product packaging) (the “Documentation”), that accompanies this License Agreement is the property of Arsenal and is protected by copyright law. Your purchase or trial use of the disks or files containing the Software transfers no title to the Software itself. Instead, any use of such Software must be made in accordance with the terms of this License Agreement. Arsenal reserves all rights not specifically granted herein. Your rights and obligations under this License Agreement are set forth below. If You violate such obligations, this License Agreement and Your right to use the Software terminate immediately. \n\nPermitted Uses of a Free Mode License. Under this License, Arsenal grants You the right to use the Software, which may be limited in its functionality or output, and documentation without charge. This License Agreement governs any releases, revisions, updates or enhancements to the Software that Arsenal may make available to You. Under the Free Mode License, Arsenal grants You the right to use the Software on any Workstation.\n\nProhibited Uses of a Free Mode License. Under this License, You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software.\n\nPermitted Uses of an Educational License. Under this License, Arsenal grants You the right to use the Software and documentation without charge (professors and students in digital forensics programs at colleges and universities) for a specific time period, after which time You will have acquired another Educational license from Arsenal and will continue to be bound by the terms of this Agreement or will have discontinued all use of the Software and destroyed it and all copies thereof, in which event all of Your rights hereunder shall end. During the Educational License period, the Software may be used for educational (and not commercial or other, e.g. casework) purposes only. During the Educational License period, Arsenal grants You the right to: (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement. \n\nProhibited Uses of an Educational License. During the Educational License period You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software; (iii) to make any copy of all or a part of the Software other than one copy (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License; or (iv) use the Software on a network or across multiple Workstations unless You have a licensed copy of the Software for each Workstation that can access the Software over that network.\n\nPermitted Uses of a Trial License. Under this License, Arsenal grants You the right to use the Software and documentation without charge for a trial period, by which time You will have purchased the software and will continue to be bound by the terms of this Agreement or will have discontinued all use of the Software and destroyed it and all copies thereof, in which event all of Your rights hereunder shall end. During the trial period, the Software may only be used for Your internal testing purposes only. During the trial period, Arsenal grants You the right to: (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement. \n\nProhibited Uses of a Trial License. During the trial period You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software; (iii) to make any copy of all or a part of the Software other than one copy (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License; or (iv) use the Software on a network or across multiple Workstations unless You have a licensed copy of the Software for each Workstation that can access the Software over that network.\n\nPermitted Uses of a Purchased License. This License Agreement governs any releases, revisions, updates or enhancements to the Software that Arsenal may make available to You. Once fully purchased, under this License Agreement, Arsenal grants You the right to (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement; (iii) use the Software on a network, provided that You have a licensed copy of the Software for each Workstation that can access the Software over that network; (iv) permanently transfer all of Your rights in the Software granted under this License Agreement to another person or entity, provided that You retain no copies of the Software and the transferee agrees to the terms of this License Agreement. Partial transfer of Your rights under this License Agreement shall not be permitted. For example, if the applicable documentation grants You the right to use multiple copies of the Software, only a transfer of the rights to use all such copies of the Software would be valid.\n\nProhibited Uses of a Purchased License. You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or transfer the Software or Your rights hereunder except as expressly provided in this License Agreement; (ii) make any copy of all or a part of the Software other than the one backup copy or as permitted above; (iii) use the Software as part of a facility management, timesharing, service provider or service bureau arrangement; (iv) publish the software for others to copy; or (v) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software, modify, translate or create derivative works of the Software, any updates, or any part thereof (except as and only to the extent any foregoing restriction is prohibited by applicable law or to the extent as may be permitted by the licensing terms governing use of any open sourced components). Any attempt to do any of the above is a violation of this License Agreement. If You breach any of these restrictions, the License Agreement shall terminate and You may be subject to prosecution and damages.\n\nPRIVACY - PURCHASING AND LICENSING. Arsenal uses third-party services for purchasing and licensing of the Software. Please view their respective privacy policies at http://www.avangate.com/legal/privacy.php and http://www.softworkz.com/privacy.html.\n\nUPDATES. Arsenal reserves the right to modify the Software at any time. Arsenal may distribute modifications or updates to the Software or to portions of the Software to reflect developments. In addition, Arsenal may distribute “bug fixes” or other revisions to the Software. You will be entitled to such updates if Arsenal, in its sole discretion, determines to make such updates available to You (generally within an active software subscription or the first year after purchase, depending on the type of license in question). The terms of this License Agreement will govern any updates unless such update is accompanied by a separate license agreement, in which case the terms of that license will govern.\n\nPRODUCT INSTALLATION AND ACTIVATION There may be technological measures in this Software that are designed to prevent unlicensed or illegal use of the Software. You agree that Arsenal may use these measures to protect against software piracy. This Software may contain enforcement technology that limits Your ability to install and uninstall the Software to not more than a finite number of times for a finite number of computers. This License Agreement and the Software containing enforcement technology may require activation as further set forth in the Documentation. If so, the Software will only operate for a finite period of time prior to Software activation by You. During activation, You may be required to provide Your unique activation code accompanying the Software to verify the authenticity of the Software. If You do not complete the activation within the finite period of time set forth in the Documentation, or as prompted by the Software, the Software will cease to function until activation is complete; at which time the Software functionality will be restored. In the event that You are not able to activate the Software, You may contact Arsenal for assistance.\n\nTermination. The license is effective until terminated by You or Arsenal. You may terminate this License at any time by discontinuing all use of the Software and destroying it and all copies thereof. Your rights under this License will terminate automatically without notice from Arsenal if You fail to comply with any term(s) of this License. Upon termination of the License, You shall cease all use of the Software, and destroy all copies, full or partial, of the Software.\n\nNO WARRANTY: YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THE SOFTWARE IS AT YOUR SOLE RISK AND THAT THE ENTIRE RISK AS TO SATISFACTORY QUALITY, PERFORMANCE, ACCURACY AND EFFORT IS WITH YOU. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE AND ANY SERVICES PERFORMED OR PROVIDED BY THE SOFTWARE (“SERVICES”) ARE PROVIDED “AS IS” AND “AS AVAILABLE”, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, AND ARSENAL HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS WITH RESPECT TO THE SOFTWARE AND ANY SERVICES, EITHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF MERCHANTABILITY, OF SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY, OF QUIET ENJOYMENT, AND NON-INFRINGEMENT OF THIRD PARTY RIGHTS. ARSENAL DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE SOFTWARE, THAT THE FUNCTIONS CONTAINED IN, OR SERVICES PERFORMED OR PROVIDED BY, THE SOFTWARE WILL MEET YOUR REQUIREMENTS, THAT THE OPERATION OF THE SOFTWARE OR SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT DEFECTS IN THE SOFTWARE OR SERVICES WILL BE CORRECTED. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY ARSENAL OR ITS AUTHORIZED REPRESENTATIVE SHALL CREATE A WARRANTY. SHOULD THE SOFTWARE OR SERVICES PROVE DEFECTIVE, YOU ASSUME THE ENTIRE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES OR LIMITATIONS ON APPLICABLE STATUTORY RIGHTS OF A CONSUMER, SO THE ABOVE EXCLUSION AND LIMITATIONS MAY NOT APPLY TO YOU.\n\nLIMITATION OF LIABILITY: TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT SHALL ARSENAL OR ANY SUPPLIER OR ANY OTHER PERSON INVOLVED IN THE CREATION, PRODUCTION, OR DISTRIBUTION OF THE SOFTWARE BE LIABLE FOR PERSONAL INJURY, OR ANY INCIDENTAL, SPECIAL, INDIRECT, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES WHATSOEVER, INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF PROFITS, LOSS OF DATA, LOSS OF SAVINGS, BUSINESS INTERRUPTION, LOSS OF BUSINESS OR PERSONAL INFORMATION OR ANY OTHER COMMERCIAL DAMAGES OR LOSSES, ARISING OUT OF OR RELATED TO YOUR USE OR INABILITY TO USE THE SOFTWARE OR QUALITY, OR PERFORMANCE OF THE SOFTWARE HOWEVER CAUSED, REGARDLESS OF THE THEORY OF LIABILITY (CONTRACT, TORT OR OTHERWISE) AND EVEN IF ARSENAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OF LIABILITY FOR PERSONAL INJURY, OR OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT APPLY TO YOU. In no event shall Arsenal’s total liability to You for all damages (other than as may be required by applicable law in cases involving personal injury) exceed the purchase price of the Software. The foregoing limitations will apply even if the above stated remedy fails of its essential purpose. These disclaimers shall apply regardless of whether You accept the Software.\n\nThe Software is not designed for and is not intended for use in hazardous environments requiring fail-safe (fault-tolerant) performance such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, weapons or defense systems, life support systems or any other context in which the failure of the Software could lead directly to death, personal injury or severe damage to property or the environment. Arsenal, the Developers of the Software and its suppliers specifically disclaim any express or implied warranty of the Software’s suitability for these types of activities.\n\nEXPORT REGULATION. You acknowledge that the Software and related technical data and services (collectively “Controlled Technology”) may be subject to the import and export laws of the United States, specifically the U.S. Export Administration Regulations (EAR), and the laws of any country where Controlled Technology is imported or re-exported. You agree to comply with all relevant laws and will not export any Controlled Technology in contravention to U.S. law nor to any prohibited country, entity, or person for which an export license or other governmental approval is required. In particular, but without limitation, the Software may not be exported or re-exported (a) into any U.S. embargoed countries or (b) to anyone on the U.S. Treasury Department's list of Specially Designated Nationals or the U.S. Department of Commerce Denied Person’s List or Entity List. By using the Software, You represent and warrant that You are not located in any such country or on any such list. USE OR FACILITATION OF ANY ARSENAL PRODUCT IN CONNECTION WITH ANY ACTIVITY INCLUDING, BUT NOT LIMITED TO, THE DESIGN, DEVELOPMENT, DESIGN, MANUFACTURE, PRODUCTION FABRICATION, TRAINING, OR TESTING OF CHEMICAL, BIOLOGICAL, OR NUCLEAR MATERIALS, OR MISSILES, DRONES, OR SPACE LAUNCH VEHICLES CAPABLE OF DELIVERING WEAPONS OF MASS DESTRUCTION IS PROHIBITED, IN ACCORDANCE WITH U.S. LAW.\n\nGovernment End Users. The Software and Documentation: Was developed with no government funds; Is a trade secret of Arsenal for all purposes of the Freedom of Information Act. The Software and any associated documentation are provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in subparagraph (c)(1)(ii) of The Rights In Technical Data and Computer Software Clause at 52.227-7013. The Software and related documentation are “Commercial Items”, as that term is defined at 48 C.F.R. §2.101, consisting of “Commercial Computer Software” and “Commercial Computer Software Documentation”, as such terms are used in 48 C.F.R. §12.212 or 48 C.F.R. §227.7202, as applicable. Consistent with 48 C.F.R. §12.212 or 48 C.F.R. §227.7202-1 through 227.7202-4, as applicable, the Commercial Computer Software and Commercial Computer Software Documentation are being licensed to U.S. Government end users (a) only as Commercial Items, and (b) with only those rights as are granted to all other end users pursuant to the terms and conditions herein. Unpublished-rights reserved under the copyright laws of the United States.\n\nGeneral: This License Agreement is the entire agreement between You and Arsenal relating to the Software and: (i) supersedes all prior or contemporaneous oral or written communications, proposals, and representations with respect to its subject matter; and (ii) prevails over any conflicting or additional terms of any quote, order, acknowledgment, or similar communications between the parties. Notwithstanding the foregoing, nothing in this License Agreement will diminish any rights You may have under existing consumer protection legislation or other applicable laws in Your jurisdiction that may not be waived by contract. Arsenal may assign its rights and obligations under this Agreement, without notice, to any affiliate of Arsenal or to any party (or its affiliate) acquiring Arsenal (or any affiliate of Arsenal to which this Agreement has been assigned) or acquiring all or substantially all of the assets to which this Agreement applies. All updates or new versions of the Software which may be received by You from Arsenal shall also be governed by this License Agreement. This License Agreement and Your use of the Software shall be construed, interpreted and governed by the laws of the State of Massachusetts. Your use of the Software may also be subject to other local, state, national, or international laws. If any provision of this License Agreement is found void or unenforceable, it will not affect the validity of the rest of this License. The disclaimers of warranties and damages and limitations on liability shall survive termination. This License Agreement may only be modified by the Documentation or by a written document that has been signed by both You and Arsenal. Headings and captions are for convenience only and are not to be used in the interpretation of this Agreement.\n\nUS Government Agencies Bound By Defense Federal Acquisition Regulations. APPLICABLE FLOWDOWNS OF THE PRIME CONTRACT. H.11.3 GSAM 552.232-39 Unenforceability of Unauthorized Obligations (FAR Deviation) (July 2015). (a) Except as stated in paragraph (b) of this clause, when any supply or service acquired under this contract is subject to any commercial supplier agreement (as defined in 502.101) that includes any clause requiring the Government to indemnify the Contractor or any person or entity for damages, costs, fees, or any other loss or liability that would create an Anti-Deficiency Act violation (31 U.S.C. 1341), the following shall govern: (1) Any such clause is unenforceable against the Government. (2) Neither the Government nor any Government authorized end user shall be deemed to have agreed to such clause by virtue of it appearing in the commercial supplier agreement. If the commercial supplier agreement is invoked through an “I agree” click box or other comparable mechanism (e.g., “click-wrap” or “browse-wrap” agreements), execution does not bind the Government or any Government authorized end user to such clause. (3) Any such clause is deemed to be stricken from the commercial supplier agreement. (b) Paragraph (a) of this clause does not apply to indemnification by the Government that is expressly authorized by statute or applicable agency regulations and procedures. (End of Clause) H.11.4 GSAM 552.232-78 Commercial Supplier Agreements – Unenforceable Clauses (a) When any supply or service acquired under this contract is subject to a commercial supplier agreement, the following language shall be deemed incorporated into the commercial supplier agreement. As used herein, “this agreement” means the commercial supplier agreement: (1) Notwithstanding any other provision of this agreement, when the licensee/customer is an agency or instrumentality of the U.S. Government, the following shall apply: (i) Applicability. This agreement is part of a contract between the commercial supplier (or the licensor, where the commercial supplier agreement contains a license) and the U.S. Government for the acquisition of the supply or service 6 (including all contracts, task orders, and delivery orders under FAR Parts 13, 14 or 15). (ii) Licensee or Customer. This agreement shall bind the ordering activity as licensee or customer but shall not operate to bind a Government employee or person acting on behalf of the Government in his or her personal capacity. (iii) Law and disputes. This agreement is governed by Federal law. (A) Any provision purporting to subject the U.S. Government to the laws of a U.S. state, U.S. territory, district, or municipality, or foreign nation, except where Federal law expressly provides for the application of such laws, is hereby deleted. (B) Any provision requiring dispute resolution in a specific forum or venue that is different from that prescribed by applicable Federal law is hereby deleted. (C) Any provision prescribing a different time period for bringing an action than that prescribed by applicable Federal law is hereby deleted. (iv) Continued performance. In accordance with subparagraph (i) of the clause at 52.233-1 (Disputes), this agreement may not be unilaterally terminated by the commercial supplier or licensor, and the provision of supplies or services under this agreement may not be unilaterally suspended unless generally withdrawn from the commercial market. If the supplier or licensor believes the ordering activity to be in breach of the agreement, it shall pursue its rights under the Contract Disputes Act or other applicable Federal statute while continuing performance as set forth in such subparagraph (i). (v) Arbitration; equitable or injunctive relief. In the event of a claim or dispute arising under or relating to this agreement, (A) binding arbitration shall not be used unless specifically authorized by agency guidance, and (B) equitable or injunctive relief, including the award of attorney fees, costs or interest, may be awarded against the U.S. Government only when explicitly provided by statute (e.g., Prompt Payment Act or Equal Access to Justice Act). (vi) No additional terms. No other commercial supplier terms shall bind the Government unless included verbatim (not by reference) in the commercial supplier agreement and added to the Government contract or order with the approval of the cognizant contracting officer. (vii) No unilateral changes. Any clause of this agreement claiming that one party to the agreement may unilaterally change any provision of this agreement shall not apply. (viii) No automatic renewals. If any license or service tied to periodic payment is provided under this agreement (e.g., annual software maintenance or annual lease term), such license or service shall not renew automatically upon expiration of its current term without prior express Government approval. (ix) Indemnification. Any clause of this agreement requiring the commercial supplier or licensor to defend or indemnify the end user is hereby amended to provide that representation in the conduct of litigation in which the United States is a party or is interested is reserved for the U.S. Department of Justice in accordance with 28 U.S.C. 516. (x) Audits. Any clause of this agreement permitting the commercial supplier or licensor to audit the end user’s compliance with this agreement is hereby amended as follows: (A) Discrepancies found in an audit may result in a charge by the commercial supplier or licensor to the ordering activity. Any resulting invoice must comply with the proper invoicing requirements specified in the underlying Government contract or order. (B) This charge, if disputed by the ordering activity, will be resolved through the Disputes clause at 52.233-1; no payment obligation shall arise on the part of the ordering activity until the conclusion of the dispute process. (C) Any audit requested by the commercial supplier or licensor will be performed at the commercial supplier’s or licensor’s expense, without reimbursement by the Government. (xi) Taxes or surcharges. Any taxes or surcharges which the commercial supplier or licensor seeks to pass along to the Government as end user will be governed by the terms of the underlying Government contract or order and, in any event, must be submitted to the Contracting Officer for a determination of applicability prior to invoicing. (xii) Non-assignment. This agreement may not be assigned, nor may any rights or obligations thereunder be delegated, without the Government's prior approval, except as expressly permitted under the clause at 52.232-23, Assignment of Claims. (xiii) Confidential information. If this agreement includes a confidentiality clause, such clause is hereby amended to state that (A) neither this agreement nor the final pricing agreed to by the ordering activity in the underlying Government contract or order shall be deemed \"confidential information\" 8 notwithstanding marking to that effect; and (B) notwithstanding anything in this agreement to the contrary, the Government may retain any confidential information as required by law, regulation or its bona fide internal document retention procedures for legal, regulatory or compliance purposes; provided, however, that all such retained confidential information will continue to be subject to the confidentiality obligations of this agreement. (b) If any provision of this agreement conflicts or is inconsistent with the preceding subparagraph (a)(1), the provisions of subparagraph (a)(1) shall prevail to the extent of such inconsistency. (End of Clause)\n\nCONTACT: Arsenal Consulting, 120 Eastern Avenue, Unit 7, Chelsea, Massachusetts 02150;\nTel (617) ARSENAL or (617) 277-3625\n\nCopyright (C) 2023 Arsenal Consulting, Inc. All rights reserved.\n"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.deps.json",
    "content": "{\n  \"runtimeTarget\": {\n    \"name\": \".NETCoreApp,Version=v6.0\",\n    \"signature\": \"\"\n  },\n  \"compilationOptions\": {},\n  \"targets\": {\n    \".NETCoreApp,Version=v6.0\": {\n      \"ArsenalImageMounter/3.10\": {\n        \"dependencies\": {\n          \"Arsenal.ImageMounter.Cli\": \"3.10.257\",\n          \"Microsoft.Win32.TaskScheduler\": \"2.9.1.0\"\n        },\n        \"runtime\": {\n          \"ArsenalImageMounter.dll\": {}\n        },\n        \"resources\": {\n          \"de/ArsenalImageMounter.resources.dll\": {\n            \"locale\": \"de\"\n          },\n          \"fr/ArsenalImageMounter.resources.dll\": {\n            \"locale\": \"fr\"\n          },\n          \"sv/ArsenalImageMounter.resources.dll\": {\n            \"locale\": \"sv\"\n          }\n        }\n      },\n      \"Arsenal.ImageMounter/3.10.257\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Dmg\": \"1.0.28\",\n          \"LTRData.DiscUtils.Fat\": \"1.0.28\",\n          \"LTRData.DiscUtils.Ntfs\": \"1.0.28\",\n          \"LTRData.DiscUtils.OpticalDisk\": \"1.0.28\",\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"LTRData.DiscUtils.Vdi\": \"1.0.28\",\n          \"LTRData.DiscUtils.Vhd\": \"1.0.28\",\n          \"LTRData.DiscUtils.Vhdx\": \"1.0.28\",\n          \"LTRData.DiscUtils.Vmdk\": \"1.0.28\",\n          \"LTRData.DiscUtils.Xva\": \"1.0.28\",\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\",\n          \"Microsoft.Win32.Registry\": \"5.0.0\",\n          \"System.Buffers\": \"4.5.1\",\n          \"System.IO.FileSystem.AccessControl\": \"5.0.0\",\n          \"System.Memory\": \"4.5.5\",\n          \"System.ServiceProcess.ServiceController\": \"7.0.1\",\n          \"System.Threading.Tasks.Extensions\": \"4.5.4\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/Arsenal.ImageMounter.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"3.10.257.0\"\n          }\n        }\n      },\n      \"Arsenal.ImageMounter.Cli/3.10.257\": {\n        \"dependencies\": {\n          \"Arsenal.ImageMounter\": \"3.10.257\",\n          \"System.Text.Json\": \"7.0.3\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/aim_cli.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"3.10.257.0\"\n          }\n        }\n      },\n      \"Arsenal.ImageMounter.Forms/3.10.257\": {\n        \"dependencies\": {\n          \"Arsenal.ImageMounter\": \"3.10.257\",\n          \"Microsoft.Win32.Registry\": \"5.0.0\",\n          \"System.IO.FileSystem.AccessControl\": \"5.0.0\",\n          \"System.Memory\": \"4.5.5\",\n          \"System.ServiceProcess.ServiceController\": \"7.0.1\"\n        },\n        \"runtime\": {\n          \"lib/net6.0-windows7.0/Arsenal.ImageMounter.Forms.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"3.10.257.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.BootConfig/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Registry\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.BootConfig.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Btrfs/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"lzo.net\": \"0.0.6\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Btrfs.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Core/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\",\n          \"System.Text.Encoding.CodePages\": \"7.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Core.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Dmg/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"lzfse-net\": \"1.0.15\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Dmg.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Ext/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Ext.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Fat/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"System.Text.Encoding.CodePages\": \"7.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Fat.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.HfsPlus/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.HfsPlus.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Iso9660/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Iso9660.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Lvm/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Lvm.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Nfs/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Nfs.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Ntfs/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Ntfs.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.OpticalDisk/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Iso9660\": \"1.0.28\",\n          \"LTRData.DiscUtils.Udf\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.OpticalDisk.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Registry/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"System.Memory\": \"4.5.5\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Registry.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.SquashFs/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.SquashFs.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Streams/1.0.28\": {\n        \"dependencies\": {\n          \"Microsoft.Bcl.HashCode\": \"1.1.1\",\n          \"System.Memory\": \"4.5.5\",\n          \"System.Security.Cryptography.Algorithms\": \"4.3.1\",\n          \"System.Threading.Tasks.Extensions\": \"4.5.4\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Streams.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Swap/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Swap.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Udf/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Iso9660\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Udf.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Vdi/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Vdi.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Vhd/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Vhd.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Vhdx/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Vhdx.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.VirtualFileSystem/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.VirtualFileSystem.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Vmdk/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Vmdk.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Wim/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Wim.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Xfs/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Xfs.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.DiscUtils.Xva/1.0.28\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/DiscUtils.Xva.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.28.0\"\n          }\n        }\n      },\n      \"LTRData.ExFat.Core/1.0.10\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"System.Memory\": \"4.5.5\",\n          \"System.Threading.Tasks.Extensions\": \"4.5.4\",\n          \"System.ValueTuple\": \"4.5.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/ExFat.Core.dll\": {\n            \"assemblyVersion\": \"0.9.18.0\",\n            \"fileVersion\": \"0.9.18.0\"\n          }\n        }\n      },\n      \"LTRData.ExFat.DiscUtils/1.0.10\": {\n        \"dependencies\": {\n          \"LTRData.DiscUtils.Core\": \"1.0.28\",\n          \"LTRData.DiscUtils.Streams\": \"1.0.28\",\n          \"LTRData.ExFat.Core\": \"1.0.10\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/ExFat.DiscUtils.dll\": {\n            \"assemblyVersion\": \"0.9.18.0\",\n            \"fileVersion\": \"0.9.18.0\"\n          }\n        }\n      },\n      \"lzfse-net/1.0.15\": {\n        \"runtime\": {\n          \"lib/netstandard2.0/lzfse-net.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"1.0.15.34350\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/linux-arm64/native/liblzfse.so\": {\n            \"rid\": \"linux-arm64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/linux-musl-x64/native/liblzfse.so\": {\n            \"rid\": \"linux-musl-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/linux-x64/native/liblzfse.so\": {\n            \"rid\": \"linux-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/osx-x64/native/liblzfse.dylib\": {\n            \"rid\": \"osx-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/win-x64/native/lzfse.dll\": {\n            \"rid\": \"win-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/win-x86/native/lzfse.dll\": {\n            \"rid\": \"win-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"0.0.0.0\"\n          }\n        }\n      },\n      \"lzo.net/0.0.6\": {\n        \"runtime\": {\n          \"lib/netstandard2.0/lzo.net.dll\": {\n            \"assemblyVersion\": \"0.0.6.0\",\n            \"fileVersion\": \"0.0.6.0\"\n          }\n        }\n      },\n      \"Microsoft.Bcl.HashCode/1.1.1\": {\n        \"runtime\": {\n          \"lib/netcoreapp2.1/Microsoft.Bcl.HashCode.dll\": {\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"4.700.20.56604\"\n          }\n        }\n      },\n      \"Microsoft.Management.Infrastructure/2.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.Management.Infrastructure.Runtime.Unix\": \"2.0.0\",\n          \"Microsoft.Management.Infrastructure.Runtime.Win\": \"2.0.0\"\n        }\n      },\n      \"Microsoft.Management.Infrastructure.Runtime.Unix/2.0.0\": {\n        \"runtimeTargets\": {\n          \"runtimes/unix/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"unix\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"2.0.0.0\",\n            \"fileVersion\": \"2.0.0.0\"\n          }\n        }\n      },\n      \"Microsoft.Management.Infrastructure.Runtime.Win/2.0.0\": {\n        \"runtimeTargets\": {\n          \"runtimes/win-arm/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win-arm\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win-arm\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win-arm64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win-arm64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win10-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win10-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win10-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win10-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win7-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win7-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win7-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win7-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win7-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win7-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win7-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win7-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win8-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win8-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win8-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win8-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win8-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win8-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win8-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win8-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win81-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win81-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win81-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win81-x64\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win81-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll\": {\n            \"rid\": \"win81-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win81-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll\": {\n            \"rid\": \"win81-x86\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"1.0.0.0\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win-arm\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm/native/mi.dll\": {\n            \"rid\": \"win-arm\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm/native/miutils.dll\": {\n            \"rid\": \"win-arm\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win-arm64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm64/native/mi.dll\": {\n            \"rid\": \"win-arm64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win-arm64/native/miutils.dll\": {\n            \"rid\": \"win-arm64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win10-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x64/native/mi.dll\": {\n            \"rid\": \"win10-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x64/native/miutils.dll\": {\n            \"rid\": \"win10-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win10-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x86/native/mi.dll\": {\n            \"rid\": \"win10-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win10-x86/native/miutils.dll\": {\n            \"rid\": \"win10-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.18362.1\"\n          },\n          \"runtimes/win7-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win7-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win7-x64/native/mi.dll\": {\n            \"rid\": \"win7-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win7-x64/native/miutils.dll\": {\n            \"rid\": \"win7-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win7-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win7-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win7-x86/native/mi.dll\": {\n            \"rid\": \"win7-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win7-x86/native/miutils.dll\": {\n            \"rid\": \"win7-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"10.0.14394.1000\"\n          },\n          \"runtimes/win8-x64/native/mi.dll\": {\n            \"rid\": \"win8-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.2.9200.22812\"\n          },\n          \"runtimes/win8-x64/native/miutils.dll\": {\n            \"rid\": \"win8-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.2.9200.22812\"\n          },\n          \"runtimes/win8-x86/native/mi.dll\": {\n            \"rid\": \"win8-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.2.9200.22812\"\n          },\n          \"runtimes/win8-x86/native/miutils.dll\": {\n            \"rid\": \"win8-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.2.9200.22812\"\n          },\n          \"runtimes/win81-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win81-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          },\n          \"runtimes/win81-x64/native/mi.dll\": {\n            \"rid\": \"win81-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          },\n          \"runtimes/win81-x64/native/miutils.dll\": {\n            \"rid\": \"win81-x64\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          },\n          \"runtimes/win81-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll\": {\n            \"rid\": \"win81-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          },\n          \"runtimes/win81-x86/native/mi.dll\": {\n            \"rid\": \"win81-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          },\n          \"runtimes/win81-x86/native/miutils.dll\": {\n            \"rid\": \"win81-x86\",\n            \"assetType\": \"native\",\n            \"fileVersion\": \"6.3.9600.16384\"\n          }\n        }\n      },\n      \"Microsoft.NETCore.Platforms/5.0.0\": {},\n      \"Microsoft.NETCore.Targets/1.1.0\": {},\n      \"Microsoft.Win32.Registry/5.0.0\": {\n        \"dependencies\": {\n          \"System.Security.AccessControl\": \"5.0.0\",\n          \"System.Security.Principal.Windows\": \"5.0.0\"\n        }\n      },\n      \"Microsoft.Win32.SystemEvents/4.7.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\"\n        }\n      },\n      \"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.native.System.Security.Cryptography.Apple/4.3.1\": {\n        \"dependencies\": {\n          \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple\": \"4.3.1\"\n        }\n      },\n      \"runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n        \"dependencies\": {\n          \"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\",\n          \"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\"\n        }\n      },\n      \"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple/4.3.1\": {},\n      \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {},\n      \"System.Buffers/4.5.1\": {},\n      \"System.CodeDom/4.7.0\": {},\n      \"System.Collections/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Collections.Concurrent/4.3.0\": {\n        \"dependencies\": {\n          \"System.Collections\": \"4.3.0\",\n          \"System.Diagnostics.Debug\": \"4.3.0\",\n          \"System.Diagnostics.Tracing\": \"4.3.0\",\n          \"System.Globalization\": \"4.3.0\",\n          \"System.Reflection\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Extensions\": \"4.3.0\",\n          \"System.Threading\": \"4.3.0\",\n          \"System.Threading.Tasks\": \"4.3.0\"\n        }\n      },\n      \"System.Diagnostics.Debug/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Diagnostics.EventLog/7.0.0\": {\n        \"runtime\": {\n          \"lib/net6.0/System.Diagnostics.EventLog.dll\": {\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.Messages.dll\": {\n            \"rid\": \"win\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"0.0.0.0\"\n          },\n          \"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.dll\": {\n            \"rid\": \"win\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        }\n      },\n      \"System.Diagnostics.Tracing/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Drawing.Common/4.7.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.Win32.SystemEvents\": \"4.7.0\"\n        }\n      },\n      \"System.Globalization/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.IO/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Text.Encoding\": \"4.3.0\",\n          \"System.Threading.Tasks\": \"4.3.0\"\n        }\n      },\n      \"System.IO.FileSystem.AccessControl/5.0.0\": {\n        \"dependencies\": {\n          \"System.Security.AccessControl\": \"5.0.0\",\n          \"System.Security.Principal.Windows\": \"5.0.0\"\n        }\n      },\n      \"System.Linq/4.3.0\": {\n        \"dependencies\": {\n          \"System.Collections\": \"4.3.0\",\n          \"System.Diagnostics.Debug\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Extensions\": \"4.3.0\"\n        }\n      },\n      \"System.Management/4.7.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.Win32.Registry\": \"5.0.0\",\n          \"System.CodeDom\": \"4.7.0\"\n        },\n        \"runtime\": {\n          \"lib/netstandard2.0/System.Management.dll\": {\n            \"assemblyVersion\": \"4.0.1.0\",\n            \"fileVersion\": \"4.700.19.56404\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/win/lib/netcoreapp2.0/System.Management.dll\": {\n            \"rid\": \"win\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"4.0.1.0\",\n            \"fileVersion\": \"4.700.19.56404\"\n          }\n        }\n      },\n      \"System.Memory/4.5.5\": {},\n      \"System.Reflection/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.IO\": \"4.3.0\",\n          \"System.Reflection.Primitives\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Reflection.Primitives/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Resources.Extensions/7.0.0\": {\n        \"runtime\": {\n          \"lib/net6.0/System.Resources.Extensions.dll\": {\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        }\n      },\n      \"System.Resources.ResourceManager/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Globalization\": \"4.3.0\",\n          \"System.Reflection\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Runtime/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\"\n        }\n      },\n      \"System.Runtime.CompilerServices.Unsafe/6.0.0\": {},\n      \"System.Runtime.Extensions/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Runtime.Handles/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Runtime.InteropServices/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Reflection\": \"4.3.0\",\n          \"System.Reflection.Primitives\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Handles\": \"4.3.0\"\n        }\n      },\n      \"System.Runtime.Numerics/4.3.0\": {\n        \"dependencies\": {\n          \"System.Globalization\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Extensions\": \"4.3.0\"\n        }\n      },\n      \"System.Security.AccessControl/5.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"System.Security.Principal.Windows\": \"5.0.0\"\n        }\n      },\n      \"System.Security.Cryptography.Algorithms/4.3.1\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"System.Collections\": \"4.3.0\",\n          \"System.IO\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Extensions\": \"4.3.0\",\n          \"System.Runtime.Handles\": \"4.3.0\",\n          \"System.Runtime.InteropServices\": \"4.3.0\",\n          \"System.Runtime.Numerics\": \"4.3.0\",\n          \"System.Security.Cryptography.Encoding\": \"4.3.0\",\n          \"System.Security.Cryptography.Primitives\": \"4.3.0\",\n          \"System.Text.Encoding\": \"4.3.0\",\n          \"runtime.native.System.Security.Cryptography.Apple\": \"4.3.1\",\n          \"runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\"\n        }\n      },\n      \"System.Security.Cryptography.Encoding/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"System.Collections\": \"4.3.0\",\n          \"System.Collections.Concurrent\": \"4.3.0\",\n          \"System.Linq\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Runtime.Extensions\": \"4.3.0\",\n          \"System.Runtime.Handles\": \"4.3.0\",\n          \"System.Runtime.InteropServices\": \"4.3.0\",\n          \"System.Security.Cryptography.Primitives\": \"4.3.0\",\n          \"System.Text.Encoding\": \"4.3.0\",\n          \"runtime.native.System.Security.Cryptography.OpenSsl\": \"4.3.2\"\n        }\n      },\n      \"System.Security.Cryptography.Primitives/4.3.0\": {\n        \"dependencies\": {\n          \"System.Diagnostics.Debug\": \"4.3.0\",\n          \"System.Globalization\": \"4.3.0\",\n          \"System.IO\": \"4.3.0\",\n          \"System.Resources.ResourceManager\": \"4.3.0\",\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Threading\": \"4.3.0\",\n          \"System.Threading.Tasks\": \"4.3.0\"\n        }\n      },\n      \"System.Security.Permissions/4.7.0\": {\n        \"dependencies\": {\n          \"System.Security.AccessControl\": \"5.0.0\",\n          \"System.Windows.Extensions\": \"4.7.0\"\n        }\n      },\n      \"System.Security.Principal.Windows/5.0.0\": {},\n      \"System.ServiceProcess.ServiceController/7.0.1\": {\n        \"dependencies\": {\n          \"System.Diagnostics.EventLog\": \"7.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/System.ServiceProcess.ServiceController.dll\": {\n            \"assemblyVersion\": \"7.0.0.1\",\n            \"fileVersion\": \"7.0.723.27404\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/win/lib/net6.0/System.ServiceProcess.ServiceController.dll\": {\n            \"rid\": \"win\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"7.0.0.1\",\n            \"fileVersion\": \"7.0.723.27404\"\n          }\n        }\n      },\n      \"System.Text.Encoding/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Text.Encoding.CodePages/7.0.0\": {\n        \"dependencies\": {\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/System.Text.Encoding.CodePages.dll\": {\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/win/lib/net6.0/System.Text.Encoding.CodePages.dll\": {\n            \"rid\": \"win\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        }\n      },\n      \"System.Text.Encodings.Web/7.0.0\": {\n        \"dependencies\": {\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/System.Text.Encodings.Web.dll\": {\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        },\n        \"runtimeTargets\": {\n          \"runtimes/browser/lib/net6.0/System.Text.Encodings.Web.dll\": {\n            \"rid\": \"browser\",\n            \"assetType\": \"runtime\",\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.22.51805\"\n          }\n        }\n      },\n      \"System.Text.Json/7.0.3\": {\n        \"dependencies\": {\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\",\n          \"System.Text.Encodings.Web\": \"7.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/System.Text.Json.dll\": {\n            \"assemblyVersion\": \"7.0.0.0\",\n            \"fileVersion\": \"7.0.723.27404\"\n          }\n        }\n      },\n      \"System.Threading/4.3.0\": {\n        \"dependencies\": {\n          \"System.Runtime\": \"4.3.0\",\n          \"System.Threading.Tasks\": \"4.3.0\"\n        }\n      },\n      \"System.Threading.Tasks/4.3.0\": {\n        \"dependencies\": {\n          \"Microsoft.NETCore.Platforms\": \"5.0.0\",\n          \"Microsoft.NETCore.Targets\": \"1.1.0\",\n          \"System.Runtime\": \"4.3.0\"\n        }\n      },\n      \"System.Threading.Tasks.Extensions/4.5.4\": {},\n      \"System.ValueTuple/4.5.0\": {},\n      \"System.Windows.Extensions/4.7.0\": {\n        \"dependencies\": {\n          \"System.Drawing.Common\": \"4.7.0\"\n        }\n      },\n      \"Microsoft.Win32.TaskScheduler/2.9.1.0\": {\n        \"runtime\": {\n          \"Microsoft.Win32.TaskScheduler.dll\": {\n            \"assemblyVersion\": \"2.9.1.0\",\n            \"fileVersion\": \"2.9.1.0\"\n          }\n        }\n      }\n    }\n  },\n  \"libraries\": {\n    \"ArsenalImageMounter/3.10\": {\n      \"type\": \"project\",\n      \"serviceable\": false,\n      \"sha512\": \"\"\n    },\n    \"Arsenal.ImageMounter/3.10.257\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-Q0V2D2eIXvHyDKK092xduJTWdUtubjVp1PB3jhoDvHayVwKjh8bz83KLQcYYaJN16ul0z61O28Pdh8te2p5gOw==\",\n      \"path\": \"arsenal.imagemounter/3.10.257\",\n      \"hashPath\": \"arsenal.imagemounter.3.10.257.nupkg.sha512\"\n    },\n    \"Arsenal.ImageMounter.Cli/3.10.257\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-h0ZELwbcHG9/EoosMAK9GhYuBzuizH4rw6rpooh1amzoXzXSp53dd9wahtbhBv5lKIvd9p4g4LhEutOZwT4t/w==\",\n      \"path\": \"arsenal.imagemounter.cli/3.10.257\",\n      \"hashPath\": \"arsenal.imagemounter.cli.3.10.257.nupkg.sha512\"\n    },\n    \"Arsenal.ImageMounter.Forms/3.10.257\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-hOi4lbM+VTbnuwBabu3RIJEdqRWniPfNZImNdK3s6XAIs0/29edOvQ088BiQSmuLF7AlI8gl2zdGlgJ1LsCNCA==\",\n      \"path\": \"arsenal.imagemounter.forms/3.10.257\",\n      \"hashPath\": \"arsenal.imagemounter.forms.3.10.257.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.BootConfig/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-jbdyr3N3WOSSuAIdJEv0lzWLqRTsGP6HyWIiovGo4BkF65Mj6jgm9qsfYGDRrUYnx3ME1mx2UGhSB0zGjUey2A==\",\n      \"path\": \"ltrdata.discutils.bootconfig/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.bootconfig.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Btrfs/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-043Jbrsn9ufjuYFI7+1/elCBEn+mRX0j+Ra9Jati/87FHfbXfsy9eY4xAaIYtbppE2DBzwrJu7us7Am1t4ofcQ==\",\n      \"path\": \"ltrdata.discutils.btrfs/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.btrfs.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Core/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-4DePAB6i7D8sejFDWbKVht15CZ6bRUIuICVdsOFtoBswE/HvUaPHcL9gsyj+UxQ1/hyacMdXKHsQq2S3qrCt0g==\",\n      \"path\": \"ltrdata.discutils.core/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.core.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Dmg/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-ihK4qhJsNChG2rQ2EUzyvNwDqJCTN3MSwrNDwbrzZCoxkYHUHVztI1Qdc2we40fdz6ZG0areD/hbcrwtRRbgkQ==\",\n      \"path\": \"ltrdata.discutils.dmg/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.dmg.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Ext/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-Gi8Mt1qrOzr04OFnZ0DRLRCtOrd+UE2DF3SSfTAAZHCNESYkdwCqJgpDN3J6atcq774vv8My8d5peybqkVIW8w==\",\n      \"path\": \"ltrdata.discutils.ext/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.ext.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Fat/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-wP/EUaDI6oDV8Hi45qznv/DMQ59b5Owb79MSdss335xHPA6qi6x0XjTkJ24f07hFPXUGxdMvq3mGH/YknALrVw==\",\n      \"path\": \"ltrdata.discutils.fat/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.fat.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.HfsPlus/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-uwGYBPXo+kqpAcA0EX7C+HrzZxzeCfFUjwBv6EcwcppnBF10ebr753Zx0OGZ/tz459ijhsybXZpOJHu1i50olg==\",\n      \"path\": \"ltrdata.discutils.hfsplus/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.hfsplus.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Iso9660/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-AaWwEub+uKnOXtW+K7XbYoItFUhdC8TLOdrhi91navA5estrPF+HInfFQ3NjshqwiLrNjAex1FU/IY9da5xRVQ==\",\n      \"path\": \"ltrdata.discutils.iso9660/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.iso9660.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Lvm/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-FTZxyHa2pNppVLIPdXTHROBVhFpzPuN6BOjvgNvPeFKwhG69+3P8ngdJ5CdqnZJl5SWDhq3m2AXQ1ztx8dNuRg==\",\n      \"path\": \"ltrdata.discutils.lvm/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.lvm.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Nfs/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-tFCJvrqYYYBeMrpgfzteAvgAzT26Sg0auobNoj6bfV5DGkSwkgrLBO2oPL/b7QRw3gG/Z9nLozw4iY7TSCKvfw==\",\n      \"path\": \"ltrdata.discutils.nfs/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.nfs.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Ntfs/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-FZ6UCnT335DQgGI9KB6jhGz0NGbduczTGIl5NxoLXe33ejlJHgh6jf9IKCvy3AkT9Nxl8HDEhTIbpU48V8c09g==\",\n      \"path\": \"ltrdata.discutils.ntfs/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.ntfs.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.OpticalDisk/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-LKOmjlBh9sj3lnCZZnO+4mcB7kbEnB0G+L00o78JDyt5zaEoVoVrMQ6vNLyCq5rm0Ermbm7yeR2wyTzb0hKVuQ==\",\n      \"path\": \"ltrdata.discutils.opticaldisk/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.opticaldisk.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Registry/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-NLzwxfS/eVri15blNVjGqQs/uga9eaem+Esh7h5bG6nAKZfee1bqV45mIHcMrmP2YYuGhPFRj4+Img75RK5iuA==\",\n      \"path\": \"ltrdata.discutils.registry/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.registry.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.SquashFs/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-RJpaZg0z7DIzK4bJCu2+qnemqGDaByChKO1ixZO24EsI2xLQzEjiF/2MK5k/Eoq7+jlFvJux/eTCrc2KSHYKRg==\",\n      \"path\": \"ltrdata.discutils.squashfs/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.squashfs.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Streams/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-x4MqHJqay5I13fF0DPNZz4NE3MOtRPf69wVcFxwMI5F58IPHPGC5jJPoumblCjHKOVCZoDE8UcVbpi0xmZ68bg==\",\n      \"path\": \"ltrdata.discutils.streams/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.streams.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Swap/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-7DfIy7uNRbQv4o5dZVbfnW/rLsSfdUroLPa/Cx1EAFNBKDbJOceHIoR1Eb1tEaXcaLSXVIAciLLNU2eBj70Pag==\",\n      \"path\": \"ltrdata.discutils.swap/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.swap.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Udf/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-WfU8WjXJygQU1t2dsbuxHh19dcTWwLrxSyHOL65pAF4UiuMdxgPH4eSmmNvmVC4un88DAuQta0hAjwsrOZkyfg==\",\n      \"path\": \"ltrdata.discutils.udf/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.udf.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Vdi/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-UeB8DwnMnDWFFaTdpXMN4QbFXNsx0XFujN+5i3Ua9Bb9h4beb+GJhyUPzw0uDy2qySBUhXk4SXbG50dA7+8NUg==\",\n      \"path\": \"ltrdata.discutils.vdi/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.vdi.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Vhd/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-DdJ4RtHL2bnXrSjJzAPRZDnBpUnaR0TGLqL1ETMw0N6E5/5HmzP3VNTuPklOWmmqpwdKBzC5tHzyCVKC78JWLA==\",\n      \"path\": \"ltrdata.discutils.vhd/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.vhd.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Vhdx/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-c/dg3XYOhG49kQT515vkeOpyBgvgYFOcH2YIiHhKIgBbgjcjVEgUHBTzMdhvW4yPPlEXRyOFRIG52rLUTyhsWA==\",\n      \"path\": \"ltrdata.discutils.vhdx/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.vhdx.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.VirtualFileSystem/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-1iXZ+fv80k57J8tjsJiXERP3M/8OQJqAxwmnYLRB7lX/zU5Z8loFSZJv/b7rT4+X/riUOG5FDIxPqjbLgACzAg==\",\n      \"path\": \"ltrdata.discutils.virtualfilesystem/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.virtualfilesystem.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Vmdk/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-eHtoRh9an72RSQbb6R2ud9xynRLVPHhlPIXueQblaQjRBD/snd/Csoo0M3l7vKSaHoYP3+r2wVlkz8Ap7sshsw==\",\n      \"path\": \"ltrdata.discutils.vmdk/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.vmdk.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Wim/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-oD0XtOLe3jlFcWwhaPBK5bD6TFcIJhB0G2HDj2bUN8JS9UohE4k77YAmRNsLSzgpD8Mu1guNbeV3M1poSmErXA==\",\n      \"path\": \"ltrdata.discutils.wim/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.wim.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Xfs/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-5ut6VvhTyMNaoJGdgNX89T2xgAB/WTAiDYVuUEA/bkTjj9xe4CnlbgbD08vkzQZ5zuM3X1Ab/HD1L0P7tTDo1A==\",\n      \"path\": \"ltrdata.discutils.xfs/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.xfs.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.DiscUtils.Xva/1.0.28\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-mgNTc7Zngzr30P2rVWSXU071fIJLkrOLuQbuiyP6dg17jt5n70h2XzGIbgZZDnHFP0XPMwjf2VHISzMYVylL8g==\",\n      \"path\": \"ltrdata.discutils.xva/1.0.28\",\n      \"hashPath\": \"ltrdata.discutils.xva.1.0.28.nupkg.sha512\"\n    },\n    \"LTRData.ExFat.Core/1.0.10\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-b6UUS/uH4Hd5RyUqfMMKbWwLniaP7BIBiynzmIfdkDIs6RO1rV5ZUvqU2RGepVmIsERM663vk6TbqEAfEdMqGg==\",\n      \"path\": \"ltrdata.exfat.core/1.0.10\",\n      \"hashPath\": \"ltrdata.exfat.core.1.0.10.nupkg.sha512\"\n    },\n    \"LTRData.ExFat.DiscUtils/1.0.10\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-mStJk3BrgOzdI8Ty9CKR1TtExC+IZ0y534CNJl3idP38I7Rxu/M79BZgnwjGJSK9Qazb3Oy7fRRgPtbcVCQNoA==\",\n      \"path\": \"ltrdata.exfat.discutils/1.0.10\",\n      \"hashPath\": \"ltrdata.exfat.discutils.1.0.10.nupkg.sha512\"\n    },\n    \"lzfse-net/1.0.15\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-O5AhPNxnNhI9Ena8b+VJ1hhgNac8rRmd1rnYopSEWHetU/1Z5xOFPuWT4nxpy2u5Qk7evS5kFj1dRDhcArBtpg==\",\n      \"path\": \"lzfse-net/1.0.15\",\n      \"hashPath\": \"lzfse-net.1.0.15.nupkg.sha512\"\n    },\n    \"lzo.net/0.0.6\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-d+F4Kf+Mnh3G83NOxXyxlZljkFvD5ZM7WUPu5Y7DTZousSgz1EAhpanSbj0+25KVrInroNoV/W9QqwtRH3avig==\",\n      \"path\": \"lzo.net/0.0.6\",\n      \"hashPath\": \"lzo.net.0.0.6.nupkg.sha512\"\n    },\n    \"Microsoft.Bcl.HashCode/1.1.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==\",\n      \"path\": \"microsoft.bcl.hashcode/1.1.1\",\n      \"hashPath\": \"microsoft.bcl.hashcode.1.1.1.nupkg.sha512\"\n    },\n    \"Microsoft.Management.Infrastructure/2.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-IaKZRNBBv3sdrmBWd+aqwHq8cVHk/3WgWFAN/dt40MRY9rbtHiDfTTmaEN0tGTmQqGCGDo/ncntA8MvFMvcsRw==\",\n      \"path\": \"microsoft.management.infrastructure/2.0.0\",\n      \"hashPath\": \"microsoft.management.infrastructure.2.0.0.nupkg.sha512\"\n    },\n    \"Microsoft.Management.Infrastructure.Runtime.Unix/2.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-p0lslMX5bdWLxO2P7ao+rjAMOB0LEwPYpzvdCQ2OEYgX2NxFpQ8ILvqPGnYlTAb53rT8gu5DyIol1HboHFYfxQ==\",\n      \"path\": \"microsoft.management.infrastructure.runtime.unix/2.0.0\",\n      \"hashPath\": \"microsoft.management.infrastructure.runtime.unix.2.0.0.nupkg.sha512\"\n    },\n    \"Microsoft.Management.Infrastructure.Runtime.Win/2.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-vjBWQeDOjgernkrOdbEgn7M70SF7hof7ORdKPSlL06Uc15+oYdth5dZju9KsgUoti/cwnkZTiwtDx/lRtay0sA==\",\n      \"path\": \"microsoft.management.infrastructure.runtime.win/2.0.0\",\n      \"hashPath\": \"microsoft.management.infrastructure.runtime.win.2.0.0.nupkg.sha512\"\n    },\n    \"Microsoft.NETCore.Platforms/5.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==\",\n      \"path\": \"microsoft.netcore.platforms/5.0.0\",\n      \"hashPath\": \"microsoft.netcore.platforms.5.0.0.nupkg.sha512\"\n    },\n    \"Microsoft.NETCore.Targets/1.1.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==\",\n      \"path\": \"microsoft.netcore.targets/1.1.0\",\n      \"hashPath\": \"microsoft.netcore.targets.1.1.0.nupkg.sha512\"\n    },\n    \"Microsoft.Win32.Registry/5.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==\",\n      \"path\": \"microsoft.win32.registry/5.0.0\",\n      \"hashPath\": \"microsoft.win32.registry.5.0.0.nupkg.sha512\"\n    },\n    \"Microsoft.Win32.SystemEvents/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-mtVirZr++rq+XCDITMUdnETD59XoeMxSpLRIII7JRI6Yj0LEDiO1pPn0ktlnIj12Ix8bfvQqQDMMIF9wC98oCA==\",\n      \"path\": \"microsoft.win32.systemevents/4.7.0\",\n      \"hashPath\": \"microsoft.win32.systemevents.4.7.0.nupkg.sha512\"\n    },\n    \"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==\",\n      \"path\": \"runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==\",\n      \"path\": \"runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==\",\n      \"path\": \"runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.native.System.Security.Cryptography.Apple/4.3.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-UPrVPlqPRSVZaB4ADmbsQ77KXn9ORiWXyA1RP2W2+byCh3bhgT1bQz0jbeOoog9/2oTQ5wWZSDSMeb74MjezcA==\",\n      \"path\": \"runtime.native.system.security.cryptography.apple/4.3.1\",\n      \"hashPath\": \"runtime.native.system.security.cryptography.apple.4.3.1.nupkg.sha512\"\n    },\n    \"runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==\",\n      \"path\": \"runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==\",\n      \"path\": \"runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==\",\n      \"path\": \"runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple/4.3.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-t15yGf5r6vMV1rB5O6TgfXKChtCaN3niwFw44M2ImX3eZ8yzueplqMqXPCbWzoBDHJVz9fE+9LFUGCsUmS2Jgg==\",\n      \"path\": \"runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple/4.3.1\",\n      \"hashPath\": \"runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple.4.3.1.nupkg.sha512\"\n    },\n    \"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==\",\n      \"path\": \"runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==\",\n      \"path\": \"runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==\",\n      \"path\": \"runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==\",\n      \"path\": \"runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==\",\n      \"path\": \"runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.2\",\n      \"hashPath\": \"runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512\"\n    },\n    \"System.Buffers/4.5.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==\",\n      \"path\": \"system.buffers/4.5.1\",\n      \"hashPath\": \"system.buffers.4.5.1.nupkg.sha512\"\n    },\n    \"System.CodeDom/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-Hs9pw/kmvH3lXaZ1LFKj3pLQsiGfj2xo3sxSzwiLlRL6UcMZUTeCfoJ9Udalvn3yq5dLlPEZzYegrTQ1/LhPOQ==\",\n      \"path\": \"system.codedom/4.7.0\",\n      \"hashPath\": \"system.codedom.4.7.0.nupkg.sha512\"\n    },\n    \"System.Collections/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==\",\n      \"path\": \"system.collections/4.3.0\",\n      \"hashPath\": \"system.collections.4.3.0.nupkg.sha512\"\n    },\n    \"System.Collections.Concurrent/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==\",\n      \"path\": \"system.collections.concurrent/4.3.0\",\n      \"hashPath\": \"system.collections.concurrent.4.3.0.nupkg.sha512\"\n    },\n    \"System.Diagnostics.Debug/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==\",\n      \"path\": \"system.diagnostics.debug/4.3.0\",\n      \"hashPath\": \"system.diagnostics.debug.4.3.0.nupkg.sha512\"\n    },\n    \"System.Diagnostics.EventLog/7.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-eUDP47obqQm3SFJfP6z+Fx2nJ4KKTQbXB4Q9Uesnzw9SbYdhjyoGXuvDn/gEmFY6N5Z3bFFbpAQGA7m6hrYJCw==\",\n      \"path\": \"system.diagnostics.eventlog/7.0.0\",\n      \"hashPath\": \"system.diagnostics.eventlog.7.0.0.nupkg.sha512\"\n    },\n    \"System.Diagnostics.Tracing/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==\",\n      \"path\": \"system.diagnostics.tracing/4.3.0\",\n      \"hashPath\": \"system.diagnostics.tracing.4.3.0.nupkg.sha512\"\n    },\n    \"System.Drawing.Common/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-v+XbyYHaZjDfn0ENmJEV1VYLgGgCTx1gnfOBcppowbpOAriglYgGCvFCPr2EEZyBvXlpxbEsTwkOlInl107ahA==\",\n      \"path\": \"system.drawing.common/4.7.0\",\n      \"hashPath\": \"system.drawing.common.4.7.0.nupkg.sha512\"\n    },\n    \"System.Globalization/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==\",\n      \"path\": \"system.globalization/4.3.0\",\n      \"hashPath\": \"system.globalization.4.3.0.nupkg.sha512\"\n    },\n    \"System.IO/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==\",\n      \"path\": \"system.io/4.3.0\",\n      \"hashPath\": \"system.io.4.3.0.nupkg.sha512\"\n    },\n    \"System.IO.FileSystem.AccessControl/5.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==\",\n      \"path\": \"system.io.filesystem.accesscontrol/5.0.0\",\n      \"hashPath\": \"system.io.filesystem.accesscontrol.5.0.0.nupkg.sha512\"\n    },\n    \"System.Linq/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==\",\n      \"path\": \"system.linq/4.3.0\",\n      \"hashPath\": \"system.linq.4.3.0.nupkg.sha512\"\n    },\n    \"System.Management/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-IY+uuGhgzWiCg21i8IvQeY/Z7m1tX8VuPF+ludfn7iTCaccTtJo5HkjZbBEL8kbBubKhAKKtNXr7uMtmAc28Pw==\",\n      \"path\": \"system.management/4.7.0\",\n      \"hashPath\": \"system.management.4.7.0.nupkg.sha512\"\n    },\n    \"System.Memory/4.5.5\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==\",\n      \"path\": \"system.memory/4.5.5\",\n      \"hashPath\": \"system.memory.4.5.5.nupkg.sha512\"\n    },\n    \"System.Reflection/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==\",\n      \"path\": \"system.reflection/4.3.0\",\n      \"hashPath\": \"system.reflection.4.3.0.nupkg.sha512\"\n    },\n    \"System.Reflection.Primitives/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==\",\n      \"path\": \"system.reflection.primitives/4.3.0\",\n      \"hashPath\": \"system.reflection.primitives.4.3.0.nupkg.sha512\"\n    },\n    \"System.Resources.Extensions/7.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-65ufm9ABXvxRkQ//hMcUDrQXbGWkC7z0WWZAvHlQ6Qv+JmrIwHH1lmX8aXlNlXpIrT9KxDpuZPqJTVqqwzMD8Q==\",\n      \"path\": \"system.resources.extensions/7.0.0\",\n      \"hashPath\": \"system.resources.extensions.7.0.0.nupkg.sha512\"\n    },\n    \"System.Resources.ResourceManager/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==\",\n      \"path\": \"system.resources.resourcemanager/4.3.0\",\n      \"hashPath\": \"system.resources.resourcemanager.4.3.0.nupkg.sha512\"\n    },\n    \"System.Runtime/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==\",\n      \"path\": \"system.runtime/4.3.0\",\n      \"hashPath\": \"system.runtime.4.3.0.nupkg.sha512\"\n    },\n    \"System.Runtime.CompilerServices.Unsafe/6.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==\",\n      \"path\": \"system.runtime.compilerservices.unsafe/6.0.0\",\n      \"hashPath\": \"system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512\"\n    },\n    \"System.Runtime.Extensions/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==\",\n      \"path\": \"system.runtime.extensions/4.3.0\",\n      \"hashPath\": \"system.runtime.extensions.4.3.0.nupkg.sha512\"\n    },\n    \"System.Runtime.Handles/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==\",\n      \"path\": \"system.runtime.handles/4.3.0\",\n      \"hashPath\": \"system.runtime.handles.4.3.0.nupkg.sha512\"\n    },\n    \"System.Runtime.InteropServices/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==\",\n      \"path\": \"system.runtime.interopservices/4.3.0\",\n      \"hashPath\": \"system.runtime.interopservices.4.3.0.nupkg.sha512\"\n    },\n    \"System.Runtime.Numerics/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==\",\n      \"path\": \"system.runtime.numerics/4.3.0\",\n      \"hashPath\": \"system.runtime.numerics.4.3.0.nupkg.sha512\"\n    },\n    \"System.Security.AccessControl/5.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==\",\n      \"path\": \"system.security.accesscontrol/5.0.0\",\n      \"hashPath\": \"system.security.accesscontrol.5.0.0.nupkg.sha512\"\n    },\n    \"System.Security.Cryptography.Algorithms/4.3.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-DVUblnRfnarrI5olEC2B/OCsJQd0anjVaObQMndHSc43efbc88/RMOlDyg/EyY0ix5ecyZMXS8zMksb5ukebZA==\",\n      \"path\": \"system.security.cryptography.algorithms/4.3.1\",\n      \"hashPath\": \"system.security.cryptography.algorithms.4.3.1.nupkg.sha512\"\n    },\n    \"System.Security.Cryptography.Encoding/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==\",\n      \"path\": \"system.security.cryptography.encoding/4.3.0\",\n      \"hashPath\": \"system.security.cryptography.encoding.4.3.0.nupkg.sha512\"\n    },\n    \"System.Security.Cryptography.Primitives/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==\",\n      \"path\": \"system.security.cryptography.primitives/4.3.0\",\n      \"hashPath\": \"system.security.cryptography.primitives.4.3.0.nupkg.sha512\"\n    },\n    \"System.Security.Permissions/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-dkOV6YYVBnYRa15/yv004eCGRBVADXw8qRbbNiCn/XpdJSUXkkUeIvdvFHkvnko4CdKMqG8yRHC4ox83LSlMsQ==\",\n      \"path\": \"system.security.permissions/4.7.0\",\n      \"hashPath\": \"system.security.permissions.4.7.0.nupkg.sha512\"\n    },\n    \"System.Security.Principal.Windows/5.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==\",\n      \"path\": \"system.security.principal.windows/5.0.0\",\n      \"hashPath\": \"system.security.principal.windows.5.0.0.nupkg.sha512\"\n    },\n    \"System.ServiceProcess.ServiceController/7.0.1\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-rPfXTJzYU46AmWYXRATQzQQ01hICrkl3GuUHgpAr9mnUwAVSsga5x3mBxanFPlJBV9ilzqMXbQyDLJQAbyTnSw==\",\n      \"path\": \"system.serviceprocess.servicecontroller/7.0.1\",\n      \"hashPath\": \"system.serviceprocess.servicecontroller.7.0.1.nupkg.sha512\"\n    },\n    \"System.Text.Encoding/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==\",\n      \"path\": \"system.text.encoding/4.3.0\",\n      \"hashPath\": \"system.text.encoding.4.3.0.nupkg.sha512\"\n    },\n    \"System.Text.Encoding.CodePages/7.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==\",\n      \"path\": \"system.text.encoding.codepages/7.0.0\",\n      \"hashPath\": \"system.text.encoding.codepages.7.0.0.nupkg.sha512\"\n    },\n    \"System.Text.Encodings.Web/7.0.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==\",\n      \"path\": \"system.text.encodings.web/7.0.0\",\n      \"hashPath\": \"system.text.encodings.web.7.0.0.nupkg.sha512\"\n    },\n    \"System.Text.Json/7.0.3\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-AyjhwXN1zTFeIibHimfJn6eAsZ7rTBib79JQpzg8WAuR/HKDu9JGNHTuu3nbbXQ/bgI+U4z6HtZmCHNXB1QXrQ==\",\n      \"path\": \"system.text.json/7.0.3\",\n      \"hashPath\": \"system.text.json.7.0.3.nupkg.sha512\"\n    },\n    \"System.Threading/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==\",\n      \"path\": \"system.threading/4.3.0\",\n      \"hashPath\": \"system.threading.4.3.0.nupkg.sha512\"\n    },\n    \"System.Threading.Tasks/4.3.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==\",\n      \"path\": \"system.threading.tasks/4.3.0\",\n      \"hashPath\": \"system.threading.tasks.4.3.0.nupkg.sha512\"\n    },\n    \"System.Threading.Tasks.Extensions/4.5.4\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==\",\n      \"path\": \"system.threading.tasks.extensions/4.5.4\",\n      \"hashPath\": \"system.threading.tasks.extensions.4.5.4.nupkg.sha512\"\n    },\n    \"System.ValueTuple/4.5.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==\",\n      \"path\": \"system.valuetuple/4.5.0\",\n      \"hashPath\": \"system.valuetuple.4.5.0.nupkg.sha512\"\n    },\n    \"System.Windows.Extensions/4.7.0\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-CeWTdRNfRaSh0pm2gDTJFwVaXfTq6Xwv/sA887iwPTneW7oMtMlpvDIO+U60+3GWTB7Aom6oQwv5VZVUhQRdPQ==\",\n      \"path\": \"system.windows.extensions/4.7.0\",\n      \"hashPath\": \"system.windows.extensions.4.7.0.nupkg.sha512\"\n    },\n    \"Microsoft.Win32.TaskScheduler/2.9.1.0\": {\n      \"type\": \"reference\",\n      \"serviceable\": false,\n      \"sha512\": \"\"\n    }\n  }\n}"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.log",
    "content": "---------------\n2023-09-06 14:42:21\nStarting up: Application 'C:\\Users\\Radi\\Desktop\\Final_year_project\\Arsenal-Image-Mounter-v3.10.257\\ArsenalImageMounter.dll' version 3.10.257. OS = 'Microsoft Windows 10.0.19045 X64'. Framework = .NET 6.0.14 X64\n---------------\n2023-09-06 14:42:21\nLicensed mode = False\n---------------\n2023-09-06 14:42:48\nImage file 'C:\\Users\\Radi\\Desktop\\2020JimmyWilson.E01', detected GuidPartitionTable, 2 partitions.\n0x10080 - 0x1A807F (Windows Basic Data), detected 'Microsoft NTFS' (healthy).\n\n---------------\n2023-09-06 14:43:36\nExit: Application 'C:\\Users\\Radi\\Desktop\\Final_year_project\\Arsenal-Image-Mounter-v3.10.257\\ArsenalImageMounter.dll' version 3.10.257. OS = 'Microsoft Windows 10.0.19045 X64'. Framework = .NET 6.0.14 X64\n"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.runtimeconfig.json",
    "content": "{\n  \"runtimeOptions\": {\n    \"tfm\": \"net6.0\",\n    \"frameworks\": [\n      {\n        \"name\": \"Microsoft.NETCore.App\",\n        \"version\": \"6.0.0\"\n      },\n      {\n        \"name\": \"Microsoft.WindowsDesktop.App\",\n        \"version\": \"6.0.0\"\n      }\n    ],\n    \"configProperties\": {\n      \"System.Reflection.Metadata.MetadataUpdater.IsSupported\": false\n    }\n  }\n}"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.runtimeconfig.json",
    "content": "{\n  \"runtimeOptions\": {\n    \"tfm\": \"net6.0\",\n    \"framework\": {\n      \"name\": \"Microsoft.NETCore.App\",\n      \"version\": \"6.0.0\"\n    },\n    \"configProperties\": {\n      \"System.Reflection.Metadata.MetadataUpdater.IsSupported\": false\n    }\n  }\n}"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/readme.txt",
    "content": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounter\nMany Windows®-based disk image mounting solutions mount the contents of disk images as shares or partitions, rather than complete (a/k/a “physical” or “real”) disks, which limits their usefulness to digital forensics practitioners and others. Arsenal Image Mounter mounts the contents of disk images as complete disks in Windows. As far as Windows is concerned, the contents of disk images mounted by Arsenal Image Mounter are real SCSI disks, allowing users to benefit from disk-specific features like integration with Disk Manager, launching virtual machines (and then bypassing Windows authentication and DPAPI), managing BitLocker-protected volumes, mounting Volume Shadow Copies, and more.\n\nEnd Users: If Arsenal Image Mounter is run without a license, it will run in \"Free Mode\" and provide core functionality. If Arsenal Image Mounter is licensed, it will run in \"Professional Mode” with full functionality enabled.\n\nFeature highlights:\n[Free Mode] Mount raw, forensic, and virtual machine disk images as complete (a/k/a “real”) disks on Windows\n[Free Mode] Temporary write support with replayable differencing files for all supported disk image formats\n[Free Mode] Save \"physically\" mounted objects to various disk image formats\n[Free Mode] Identify (with details), unlock, fully decrypt, and disable/suspend BitLocker-protected volumes\n[Free Mode] Access disks, volumes, and Volume Shadow Copies as virtual dd files\n[Free Mode] Virtually mount optical images\n[Free Mode] RAM disk creation with either static or dynamic memory allocation\n[Free Mode] Command-line interface (CLI) executables\n[Free Mode] MBR injection, fake disk signatures, removable disk emulation, and much more\n\n[Professional Mode] Effortlessly launch virtual machines from disk images\n[Professional Mode] Extremely powerful Windows authentication and DPAPI bypasses within virtual machines\n[Professional Mode] Volume Shadow Copy mounting (standard, with Windows NTFS driver bypass, or as complete disks)\n[Professional Mode] Launch virtual machines directly from Volume Shadow Copies\n[Professional Mode] Attach to actual physical disks (fixed and removable) to leverage virtual machine launching, VSC mounting, etc.\n[Professional Mode] Write mounted disk images to physical disks with optional free space clearing\n[Professional Mode] Windows file system driver bypass (FAT, NTFS, ExFAT, HFS+, Ext2/3/4, etc.)\n[Professional Mode] Exposure of NTFS metadata, slack, and unallocated in Windows file system driver bypass mode\n[Professional Mode] Virtually mount archives and directories\n[Professional Mode] Save disk images with fully-decrypted BitLocker volumes\n\nDevelopers: Arsenal Image Mounter source code, APIs, and executables are available for royalty-free use in open source projects. Commercial projects (and other projects not licensed under an AGPL v3 compatible license) that would like to use Arsenal Image Mounter source code, APIs, and/or executables must contact Arsenal (sales@ArsenalRecon.com) to obtain alternative licensing.\n\nPlease Note: We recommend excluding/whitelisting Arsenal Image Mounter's folder and/or executables (ArsenalImageMounter.exe and aim_cli.exe) in your anti-virus software, as we have encountered situations in which anti-virus software has prevented (sometimes silently) Arsenal Image Mounter from launching successfully. When launching virtual machines with AIM, you may need to temporarily disable real-time protection and/or instruct your anti-virus software to ignore/allow the \"utilman.exe\" threat, to ensure that AIM Virtual Machine Tools will be injected into the VMs properly. In some rare cases you may need to disable your anti-virus software entirely, as we have found one anti-virus product that interferes with AIM even after proper whitelisting.\n\nDetailed feature descriptions (Mount options):\nDisk device, read only - Mount the disk image as a read-only disk device. No write operations will be allowed. Please note, when mounting a disk image containing a partition rather than a complete disk as a disk device in read only, write temporary, and write original modes, AIM will inject what is necessary (for example, a fake MBR) so that Windows will be able to properly mount the disk image as a complete disk. The disk image itself will not be modified.\n\nPlease note: Arsenal does not recommend mounting VSCs after mounting disk images in read-only mode. See the FAQ entry \"Can you provide more detail about the Windows bugs identified by Maxim Suhanov and Arsenal which sometimes result in \"missing\" Volume Shadow Copies?\" for more information.\n\nDisk device, write temporary - Mount the disk image as a writable disk device using the AIM write filter. Modifications will be written to a write-overlay differencing file and the original disk image will not be changed. Sometimes referred to as write-overlay or write-copy mode. (Note - required for launching virtual machines.) If you would like to choose an alternate location for the differencing file, check the \"Specify an alternate differential file location\" box. AIM will also ask for an alternate differential file location if the disk image you are about to mount is already open one or more times in this mode, or if writing to the same location as the disk image is not possible because the location in which the disk image is located is write protected. If you would like AIM to delete the differencing file after the disk image is unmounted, check the \"Delete differencing file after unmount\" box. This option is disabled when the \"Automatically remount at Arsenal Image Mounter startup\" box is checked. You can choose to store the differencing data within host RAM only, and not in a file, if you check the \"Store differencing data in host RAM only (not in a file)\" box - but please note, a significant amount of RAM may be required (Arsenal recommends your host have at least 32GB RAM, and at least 16GB of free physical memory when launching virtual machines) especially in extreme situations such as fully decrypting BitLocker-protected volumes.\n\nWindows file system driver bypass, read only - Mount the disk image as a virtual read-only file system, using DiscUtils rather than Windows file system drivers. This mount option is often used to bypass file system security, expose NTFS metafiles and streams, and recover deleted files. May also be useful to read files from disk images containing corrupted file systems. (Note - BitLocker-protected volumes and VSC mounting are not supported, and disk size values are an approximation of each volume's total file size (including things like multiple links to the same file and files with sparse allocation) so the size may appear larger than the expected volume size.) Single disk, non-striped, lvm/lvm2 volumes are supported. Please note that using Windows File Explorer to copy items from this mount mode may result in unexpected behavior (especially when dealing with file systems other than NTFS and FAT) because of limitations which other tools designed to copy entire folder trees (such as robocopy) do not have - for example, Windows File Explorer will silently stop copying when hitting folders named after reserved device names like \"aux\", \"con\", etc. Also, be aware of things like symbolic links and case sensitivity (again, especially when dealing with file systems other than NTFS and FAT) which may cause unexpected behavior as you copy items from this mount mode. Finally, make sure to review the FAQ \"Can you describe some of the NTFS-related things exposed by the Windows file system driver bypass mount option as well as any NTFS-related limitations?\" below when mounting NTFS file systems.\n\nDisk device, write original - Mount the disk image as a writable disk device. Caution, modifications will be written to the original disk image.\n\nWindows file system driver bypass, write original - Mount the disk image as a virtual writable file system. Caution, modifications will be written to the original disk image. This mount option bypasses file system security but does not expose most NTFS metafiles and streams. (Note - BitLocker-protected volumes and VSC mounting are not supported, and disk size values are an approximation of each volume's total file size (including things like multiple links to the same file and files with sparse allocation) so the size may appear larger than the expected volume size.) Single disk, non-striped, lvm/lvm2 volumes are supported. Please note that using Windows File Explorer to copy items from this mount mode may result in unexpected behavior (especially when dealing with file systems other than NTFS and FAT) because of limitations which other tools designed to copy entire folder trees (such as robocopy) do not have - for example, Windows File Explorer will silently stop copying when hitting folders named after reserved device names like \"aux\", \"con\", etc. Also, be aware of things like symbolic links and case sensitivity (again, especially when dealing with file systems other than NTFS and FAT) which may cause unexpected behavior as you copy items from this mount mode.\n\nPlease note: In regard to the aforementioned mount modes - Arsenal Image Mounter’s core functionality involves mounting disk images as “real” disks on Windows, so in this sense, it is similar to attaching an actual physical disk to Windows. An important thing to be aware of, especially if using Windows Explorer to navigate AIM-mounted disk images, is behavior consistent with an actual physical disk attachment such as the Recycle Bin displaying information from all mounted volumes, junction and symbolic link redirections, and other behaviors associated with using Explorer to navigate an actual physical disk.\n\nSector size - Arsenal Image Mounter will normally select the correct sector size by default, based on disk image metadata, but there are situations in which it may need to be set manually. For example, you may need to set a sector size manually if the sector size is unusual and you are dealing with a raw disk image (without metadata) or the sector size specified in disk image metadata is incorrect. \n\nFake disk signature - Report a random disk signature to Windows. Useful if the disk image contains a zeroed-out disk signature or you are attempting to mount a duplicate disk signature. (Note - requires a valid MBR and partition table. Not compatible with GPT partitions or images without a partition table.)\n\nCreate “removable” disk device - Emulate the attachment of a USB thumb drive, which may facilitate the successful mounting of disk images containing partitions rather than complete disks or disk images without partition tables. (Caution - see relevant FAQ for caveats.)\n\nAutomatically remount at Arsenal Image Mounter startup - Remount the disk image with the current mount options when AIM next starts. To retain this persistent mounting, simply quit AIM with the disk image mounted. To cancel this persistent mounting, use \"Remove\" or \"Remove all\" before quitting AIM.\n\nDetailed feature descriptions (Main screen and dropdown menus):\nMount VSCs - Mount Volume Shadow Copies (VSCs) within a disk image*1. Hidden/offline VSCs*2 will be made available (a/k/a forced online) if desired, by modifying VSC metadata on disk while the disk image is mounted in write-temporary mode*3. A list of VSCs is provided along with timestamps which allows any combination of VSCs to be mounted. The following options are available:\n•   Standard Volume Shadow Copy mount - Mount contents of VSCs using Windows NTFS driver. This option is fast, but does not expose file system metafiles and does not bypass file system security.\n•   Volume Shadow Copy mount with Windows file system driver bypass - Mount contents of VSCs using DiscUtils NTFS driver. Useful to expose NTFS metafiles and to bypass file system security.\n•   Write temporary Volume Shadow Copy mount - Mount contents of VSCs as complete disks in write-temporary mode using Windows NTFS driver. This option is useful for launching VSCs into virtual machines, but does not expose file system metafiles and does not bypass file system security. Please note, the stability of virtual machines launched from VSCs can be impacted by things like dirty filesystems and missing clusters - for example, applications may not launch as expected.\n\n*1 AIM uses Windows to interact with VSCs. It is important to be aware that Windows does not reserve space within VSCs for certain files (Registry keys to be aware of include HKLM\\System\\CurrentControlSet\\Control\\BackupRestore\\FilesNotToBackup and FilesNotToSnapshot) which can be confusing because those files appear to exist based on file system metadata. Also, data can be (and often is) missing or otherwise corrupt in VSCs due to both known (for example, the aforementioned Registry keys and live imaging) and unknown factors - keep this in mind to appropriately set expectations in terms of VSC contents.\n*2 Including those created by the host (for example, per chkdsk activity) while a disk image has been mounted in write temporary or write-original modes. AIM will provide a warning about these kinds of hidden/offline VSCs, because mounting them may result in unpredictable behavior such as deadlocks.\n*3 Onlining hidden/offline VSCs is not supported in read only mode, only write temporary and write original modes. If AIM identifies hidden/offline VSCs within encrypted volumes, even while in write-temporary mode, those volumes must be decrypted so that AIM can then modify VSC metadata and make them available.\n\nLaunch VM – Launch a Hyper-V virtual machine using the selected AIM-mounted (or attached) disk. The disk image (or actual physical disk) should be mounted in write-temporary mode before using this feature, which is designed to make booting the contents of a disk in a virtual machine more efficient, reliable, and useful than other methods. Just after selecting Launch VM, AIM will perform file system scanning (which may take some time) in order to identify dirty file systems, avoid unexpected locks, and deal with other possible issues with launching the selected disk into a VM. AIM will determine whether the disk should be launched as a Generation 1 or Generation 2 virtual machine - see https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/plan/should-i-create-a-generation-1-or-2-virtual-machine-in-hyper-v for more details about the two Generations. The virtual machine is created with half of the available physical CPU cores, half of free host RAM (maximum of 6GB if >10GB is available), two network adapters (not connected by default), one DVD-ROM (without any attached image) and the AIM-mounted/attached disk as the primary IDE or SCSI HD. The Launch VM feature currently works (with full functionality) on Windows 10/11 (and Server 2016/2019) x64 and requires that Hyper-V role be running on physical hardware, not within a virtual machine. Information from Microsoft about installing Hyper-V on Windows 10 is available at https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v. Arsenal's preference in terms of installing Hyper-V is the \"Enable Hyper-V with CMD and DISM\" method. If you are unsure whether Hyper-V is running, the output from \"sc query HvService\" at a command prompt can be helpful. Please note that hibernation files are not applied when disks are launched into virtual machines, because the same physical hardware would be required for hibernation to be applied properly - so use tools like Hibernation Recon to assist in the analysis of populated hibernation files.\n\nUpon selecting Launch VM, AIM offers various options related to launching the virtual machine. You can choose to:\n•   Configure the network connection so that it is disconnected (i.e. completely isolated), shared between VMs, shared between VMs and host, or set to the default switch with external NAT.\n•   Enable guest services to provide copy/paste and other functionality between the VM and host. This is not recommended when isolation between the VM and host is preferred.\n•   Check file systems and boot environment, repairing and adjusting as necessary. If this option is available (not greyed out), Arsenal recommends that you not deselect it.\n•   Disable/suspend BitLocker-protected volumes, so that they do not need to be unlocked again once the VM is running.\n•   Inject AIM Virtual Machine Tools and adjust boot drivers. Some disks can be difficult to boot directly into virtual machines, so AIM can inject*1 a small application into virtual machines running Windows*2. AIM Virtual Machine Tools displays a list of accounts (accounts without tangible folder structure or without crucial Registry information are not listed in AIM Virtual Machine Tools, as there would effectively be nothing to login to and/or no way to do so) and can open an administrative command prompt from the login screen. AIM Virtual Machine Tools will launch automatically on Windows XP and can be accessed via the “Ease of Access” or \"Accessibility\" icon on Windows Vista/7/8/8.1/10/11*3. Please note, the list of accounts displayed by AIM Virtual Machine Tools will sometimes include recovered passwords which are particularly important in certain situations (involving Windows 8/8.1) in which automatic logons are not available and manual logons must be performed to unlock DPAPI.\n•   Boot with last Windows shutdown time (and disable Hyper-V's automatic time updates), which is useful (for example) to avoid time-based automatic cleanup functions (such as browser history cleanup) and to deal with time-based software licensing. The last shutdown time will be displayed in UTC. Please note, the last shutdown time comes from the Registry value ShutdownTime at HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Windows, so (for example) a system which crashed and did not properly shutdown or a disk image obtained live may not have the (initially) expected last shutdown time.\n•   Bypass Windows authentication*4 using a variety of techniques, including policy adjustments, so that any password input will work. Local, Microsoft (cloud), Active Directory, and Azure Active Directory accounts, using many kinds of authentication including passwords, PINs, biometrics, images, and smart cards, are supported. Accounts will be provided with administrative privileges if requested or if the original privileges are unavailable. Please note that to access certain things in Windows like EFS-encrypted items and cached login credentials you will need to crack, and not bypass, account authentication... or perform a combination of Windows authentication and DPAPI bypasses*5. Also - if you already have credentials for a Windows account, Arsenal recommends disabling this option so (for example) if you mistype a user's password you will be alerted to that fact rather than logged in anyway.\n•   Bypass Data Protection API (DPAPI), which provides seamless access (particularly in concert with AIM's Windows authentication bypass) to the selected user's DPAPI-protected content such as website, network share, and application credentials as well as files and folders protected by Encrypting File System (EFS). DPAPI-protected content is normally made available after a user successfully logs into Windows, but AIM's DPAPI bypass makes it available without already having the user's credentials. AIM offers three DPAPI bypass modes:\n1.) Quick - This is an immediate DPAPI bypass and will be available in certain situations when AIM first launches a Windows system with local, Microsoft (cloud), and/or Azure AD accounts into a virtual machine. If AIM determines that one or more users are eligible for a quick DPAPI bypass, they will be shown in the dropdown list appended with (Quick). Please note, a quick DPAPI bypass is most effective with the last logged-on user on Windows 10 and 11 x64 (but will sometimes be available for previously logged-on users and earlier Windows versions as well), it does not persist across reboots, and in certain situations (involving Windows 8/8.1) an automatic logon is not possible even though AIM has recovered a user's password (resulting in booting to the Windows logon screen rather than the selected user's Desktop) - so the recovered password will be displayed by AIM Virtual Machine Tools for use in a manual logon, thereby unlocking DPAPI.\n2.) PIN <Already Recovered PIN> - AIM has already recovered a numeric PIN (in the brief amount of time since selecting the Launch VM button) for the associated user and displays it both in the dropdown list and within AIM Virtual Machine Tools. Once at the Windows logon screen, this PIN can be used for a manual logon, thereby unlocking DPAPI. Please note, if a Quick DPAPI bypass is also available for the associated user, AIM may use the Quick DPAPI bypass anyway and is displaying this already recovered PIN for informational purposes. Also, keep in mind that whether a PIN is found or needs to be brute forced is not only based on workstation speed but on environmental factors such as file system scanning - it is possible that a PIN is found on the first launch into a VM, but on subsequent launches (in the same Windows session) a brute force is required.\n3.) PIN Brute Force - AIM has determined that a brute force of the associated user’s numeric PIN is possible, and will provide an ETA for brute forcing after this dropdown item is selected. AIM will provide the recovered PIN within AIM Virtual Machine Tools so that it can be used for a manual logon, thereby unlocking DPAPI. Please note, if a Quick DPAPI bypass is also available for the associated user, AIM may use the Quick DPAPI bypass anyway and then the recovered PIN can be viewed by launching AIM Virtual Machine Tools within the VM by using Start/Run and then running “aimvm”.\n•   Bypass Linux authentication. AIM will attempt to bypass Linux authentication for all users so that logons (whether typical or text-based via Ctrl+Alt+F2) are possible without passwords. \n•   Advanced: Enable nested virtualization. Leverage Hyper-V's nested virtualization which is useful (for example) when using Windows Subsystem for Android within the virtual machine launched by AIM. Keep in mind the Hyper-V nested virtualization requirements: on an Intel processor with VT-x and EPT technology, the Hyper-V host must be Windows Server 2016/Windows 10 or greater and on an AMD processor the Hyper-V host must be Windows Server 2022/Windows 11 or greater.\n•   Advanced: Disable RDP (Remote Desktop Protocol) and WMI (Windows Management Instrumentation) for extreme isolation.\n•   Advanced: Start Windows kernel debugging with WinDbg.\n\n*1 AIM will perform anti-virus evasion within the virtual machine to ensure that AIM Virtual Machine Tools runs properly.\n*2 A file named \"AIM_MODIFIED.txt\" will be placed on the root of each Windows volume in which AIM Virtual Machine Tools has made adjustments. These adjustments are temporary by design, based on \"Write temporary...\" mounting.\n*3 Depending on your anti-virus software and settings, you may need to exclude/whitelist AIM's folder and/or executables (ArsenalImageMounter.exe and aim_cli.exe) and/or temporarily disable real-time protection to ensure that AIM Virtual Machine Tools will be injected properly. You may also need to instruct your anti-virus software to ignore/allow the \"utilman.exe\" threat while the VM is booting. In some rare cases you may need to disable your anti-virus software entirely, as we have found one anti-virus product that interferes with AIM even after proper whitelisting.\n*4 It is not always possible to identify an authoritative DOMAIN\\USER combination for cached domain accounts in certain states, so AIM may create a DOMAIN\\USER combination to facilitate access to those accounts. AIM Virtual Machine Tools will display AIM-created DOMAIN\\USER combinations in red.\n*5 In the case of Active Directory accounts, if a domain controller is available it is quite easy to set up a virtual network between it and the clients (all running in virtual machines launched by AIM), which will allow you to reset account passwords from the domain controller. Resetting passwords in this way will allow you access to previously inaccessible items on the clients such as cached login credentials and EFS-encrypted files and folders.\n\nAdditional notes regarding Launch VM:\n•   Arsenal officially supports launching disk images (or actual physical disks) containing Windows XP Service Pack 1 onward into virtual machines. It is often possible to launch earlier versions of Windows into virtual machines, but with limited functionality from AIM.\n•   BitLocker \"automatic device encryption\" can interfere with launching disk images (or actual physical disks) into virtual machines. This problematic BitLocker setting can sometimes be found on new Windows installations as part of the Windows \"Out Of Box Experience\" or OOBE. Disable this setting (in an OOBE scenario by activating BitLocker protection, turning BitLocker off in \"BitLocker Drive Encryption\" or in \"Device encryption\" settings, and if so desired turning BitLocker back on) before launching virtual machines from disk images or actual physical disks.\n•   The Launch VM button will change to \"Reconnect VM\" after a VM is launched in order to quickly resolve Hyper-V black screens which sometimes occur on Windows 10 hosts when the view to VMs within Hyper-V consoles is lost, but the VMs are still running fine (which can be confirmed in Hyper-V Manager).\n•   AIM will create a RAM disk (which is quickly placed offline) when launching certain combinations of operating system, architecture, and partitioning into a VM. The RAM disk created by AIM is temporary, and used to make adjustments to the boot process in situations which include launching Windows 7 (or earlier) on a GPT disk into a VM.\n•   If you would like to see whether your AIM-launched VMs are Generation 1 or Generation 2, open an administrative PowerShell and run \"get-vm | format-list Name,Generation, State\"\n\nMount archive file –  Select zip, cab, wim, tar (raw or gzip or bzip2 compressed), or AFF4 (particularly useful with AFF4-L) files to mount read only with CD/DVD-ROM emulation or as a standard file system. If an archive file is mounted as a CD/DVD-ROM, it can be saved as an ISO image or attached directly to a virtual machine, but normal limitations apply such as a 4GB file size limit and a maximum path length of about 60 characters. If an archive file is mounted as a standard file system, there is no limitation on file size or path length, but it cannot be saved as an ISO image or attached directly to a virtual machine. Note that wim files often contain more than one image, so more than one virtual drive may appear. This feature is useful (for example) when you want to perform analysis of particular items within an archive in a read-only manner and/or do not want to extract anything from the archive to the file system.\n\nMount directory – Select a directory to mount as a read-only virtual CD/DVD-ROM. Includes “Boot image” option to create a bootable CD/DVD by injecting a boot image into the area reserved by the El-Torito standard. This option is sometimes used to boot operating system installation and repair tools (which exist loose in a directory as opposed to on a bootable CD/DVD or in a bootable ISO) into a virtual machine. By default, the \"Boot image type\" option is set to \"NoEmulation\" - in other words, it will be set to the typical (and modern) way to configure a boot image. The \"Boot image type\" option can be set to use floppy or hard disk emulation if desired. This feature is useful (for example) when you want to perform analysis of particular items within a directory in a read-only manner. Also facilitates the attachment of a directory to a virtual machine as if it is a virtual CD/DVD-ROM, which is useful when attaching a directory directly is not possible or you prefer to attach the directory read only.\n\nCreate new image file - Create and mount a new disk image file with an NTFS partition. Image formats supported are the same as the \"Save as new image\" file option. If a raw format is selected, it will be created with the sparse file attribute. The new disk image will be created with 64kb partition alignment, disk and boot code signatures, and fake (but valid) boot code in MBR and VBR. This feature is useful (for example) when testing the behavior of digital forensics tools, file systems, and/or volume and disk encryption.\n\nSave as new image file - Save a “physically” mounted object, including deltas, to a new raw (.raw, .dd, .img, .ima), forensic (.e01), or virtual machine (.vhd, .vhdx, .vdi, .vmdk) disk image. This function can also be used to save virtually mounted objects (such as archives or directories mounted as CD/DVD-ROMs) as raw CD/DVD images, using the extensions .iso and .bin. Some users may leverage this feature to effectively convert from one disk image (or other source) format to another, but please note that (1) the source is saved as it currently appears to Windows - including (for example) injected MBRs and fake disk signatures (which can be applied to the current AIM session even when the disk image is mounted read only - so if this is not what you want, use the AIM CLI to perform conversions from one format to another without mounting being involved) as well as other deltas, and (2) some disk image formats (particularly virtual machine disk image formats) require unique GUIDs within their headers, so for effective hash value comparisons you will need to hash the contents of those disk images rather than the disk images themselves. Also note that when saving to a forensic (.e01) disk image, \"default\" compression and a 2TB segment size will be used.\n\nShow BitLocker status (all BitLocker-protected volumes) - Displays BitLocker protector IDs and types for all BitLocker-protected volumes within the currently selected disk. Also displays original backup locations for BitLocker recovery keys (e.g. Cloud, File, Printed) and the BitLocker recovery keys themselves (if the volume is currently unlocked).\n\nUnlock BitLocker-protected volumes - Unlocking BitLocker-protected volumes (within the currently selected disk) will decrypt their contents \"on the fly\" during the current Windows session. The BitLocker-protected data will still be stored encrypted on disk.\n\nFully decrypt BitLocker-protected volumes - Fully decrypting BitLocker-protected volumes (within the currently selected disk) will completely remove BitLocker, resulting in the previously BitLocker-protected data now being stored unencrypted on disk.\n\nDisable/suspend BitLocker-protected volumes - Disabling (a/k/a suspending) BitLocker-protected volumes (within the currently selected disk) will disable their \"protectors\" (such as passwords) to allow seamless access to them. The BitLocker-protected data will still be stored encrypted on disk. Disabled BitLocker-protected volumes are sometimes referred to as being in \"Clear Key Mode.\"\n\nSave as fully decrypted image file - Saving BitLocker-protected volumes (within the currently selected disk) to a fully decrypted image file is an efficient way (saving time and disk space) to unlock BitLocker-protected volumes and create a new disk image with any previously BitLocker-protected volumes fully decrypted. Arsenal recommends mounting the BitLockered disk image read-only before using this feature.\n\nPhysical disks - AIM's write filter can be applied not only to disk images, but to actual physical disks (fixed and removable) as well. Arsenal strongly recommends using a hardware-based write blocker between a physical disk and a forensic workstation running AIM, but we are aware that some customers use software-based write blockers and other methods (Windows auto-mount policy) to reduce interaction with physical disks. AIM's Advanced/Physical disks dropdown menu options include the following:\n•   Attach AIM to physical disk...\n•   Acquire disk image from physical disk...\n•   Write mounted disk image to physical disk...\n•   Manage Windows auto-mount policy...\nNote: \"Write mounted disk image to physical disk...\" includes optional free space clearing which will send a TRIM command to TRIM-enabled SSD disks (before writing the mounted disk image) and use traditional clearing (after writing the mounted disk image) for HDDs.\n\nAutomatically start Arsenal Image Mounter at logon - Start AIM automatically during logon.\n\nAttach to existing virtual machine - Attach a \"physically\" mounted object to a virtual machine launched by AIM. If you attach an AIM-mounted disk image, AIM-created RAM disk, or VHDX, you will be asked to place the object offline so that the virtual machine has exclusive access to it. If you attach a folder or archive mounted by AIM with CD/DVD-ROM emulation, you will not need to place the object offline as that concept is not relevant to CD/DVD-ROM emulation. Also, please note that CD/DVD-ROM emulation is inherently read only. \n\nCreate RAM disk with fixed memory allocation - Create one or more RAM disks, with fixed memory allocation, as \"real\" disks containing NTFS file systems. These RAM disks can then be attached to virtual machines, saved to any of AIM's supported disk image formats, and more. RAM disks are sometimes used to meet extreme performance requirements (which HDDs or SSDs cannot) and to temporarily store sensitive files, because they are not written to the physical disk except during hibernation.\n\nCreate dynamically allocated RAM disk from VHD template - Create one or more RAM disks, with dynamic memory allocation (memory allocated as files are added to the RAM disk and deallocated when files are deleted), as \"real\" disks containing NTFS file systems. When using this feature, a VHD image file is selected as a \"template\" for the RAM disk. The contents of the VHD will be on the RAM disk when it is created, but the VHD will not be modified when the contents of the RAM disk change. These RAM disks can then be attached to virtual machines, saved to any of AIM's supported disk image formats, and more.\n\nEnable virtual dd - Upon enabling the virtual dd function, all available disks, partitions, volumes, and VSCs (whether AIM-mounted/attached or not) will be virtually exposed in a new volume as read-only raw disk images with the “.dd” extension. Disks will be exposed by their “PhysicalDrive” number, partitions will be exposed by their \"Harddisk\" and then \"Partition\" numbers, volumes will be exposed both by their currently assigned Windows drive letter and GUID, and VSCs by their volume GUID and timestamp. Please note that (1) CD/DVD discs will be exposed with the \".iso\" extension rather than \".dd\", (2) keep \"Mount VSCs\" footnote 1 above in mind when using this feature to work with VSCs, and (3) if AIM is used to mount any of the virtual dd targets, AIM will actually mount the underlying disk, partition, volume, or VSC (with the actual target displayed in parens) in order to reduce extreme technical complexity.\n\nRescan SCSI bus - Rescanning the SCSI bus may be useful if a disk image mounted by Arsenal Image Mounter does not appear in Windows, or if a dismounted disk image continues to appear there. This is a rarely used feature which is typically used only in debug scenarios involving compatibility issues with AIM and other drivers or applications causing unexpected behavior. The \"Rescan Disks\" option in Disk Management is similar in effect, except that it will rescan all storage buses instead of the AIM virtual SCSI adapter specifically.\n\nFAQs:\nWhy is Arsenal Image Mounter different than other disk image mounting solutions?\nMany disk image mounting solutions mount the contents of disk images in Windows as shares or partitions (rather than \"complete\" disks), which limits their usefulness. Arsenal Image Mounter is the first and only open source solution for mounting the contents of disk images as complete disks in Windows. We have also developed a significant amount of functionality that is particularly useful to the digital forensics and incident response community.\n\nWhat are the requirements for running Arsenal Image Mounter?\nArsenal strongly recommends running Arsenal Image Mounter on Windows 10 (version 1703 or later), 11, or Server 2016/2019 x64 with the latest .NET 6 and on bare metal (i.e. not in a hypervisor) so that all functionality (for example, launching virtual machines and BitLocker-related functionality) works as intended. AIM will run and provide various functionality in many other environments (many users run AIM successfully within various hypervisors), but these other environments are not recommended or supported by Arsenal.\n\nHow can I increase performance from disk images mounted by Arsenal Image Mounter?\nStoring disk images on the fastest possible storage media is the most efficient way of increasing performance from disk images mounted by Arsenal Image Mounter. Here are benchmarks from launching a Windows 10 disk image (184GB in size, E01 format) into a virtual machine with AIM (all benchmark times are from clicking Launch VM through Windows logon and seeing a user’s Desktop), which demonstrate the drastic differences in performance between disk images stored on hard disk drives (HDDs) and solid-state drives (SSDs):\n•   Mounted unlocked BitLockered disk image from internal HDD - 4-6 minutes \n•   Mounted unlocked BitLockered disk image from internal SSD - 2-3 minutes\n•   Mounted fully decrypted BitLockered disk mage from internal HDD (full decryption took 40-45 minutes) - 3-4 minutes\n•   Mounted fully decrypted BitLockered disk image from internal SSD (full decryption took 10-15 minutes) - 1 minute\n\nWhat file systems does Arsenal Image Mounter support?\nWhen mounting disk images using the \"Disk Device\" mount options, Arsenal Image Mounter essentially \"hands off\" the contents of disk images to Windows as if they were real SCSI disks, so the file system drivers currently installed on Windows will be used as necessary. Arsenal has used NTFS, FAT32, ReFS, exFAT, HFS+, UFS, and EXT3 file systems contained within AIM-mounted disks successfully when the appropriate file system drivers were installed. AIM also supports bypassing Windows file system drivers and using DiscUtils file system drivers via the \"Windows file system driver bypass\" mount option.\n\nWhat disk image formats does Arsenal Image Mounter support?\n•   Raw (dd)\n•   Advanced Forensics Format 4 (AFF4) if libaff4 is available\n•   EnCase (E01 and limited support for Ex01) if libewf is available\n•   Virtual Machine Disk Files (VHD, VDI, XVA, VMDK, OVA, qcow, qcow2) and checkpoints (AVHD, AVHDX) \n\nWhat do you mean when you use the phrase \"disk images?\"\nWhen we use the phrase \"disk images\" we are using it loosely, in the sense that we are referring to images containing complete disks or partitions, whether they are in raw, virtual machine, or forensic formats.\n\nWhy are some files and folders inaccessible to me after mounting a disk image with Arsenal Image Mounter?\nArsenal Image Mounter passes the contents of disk images to Windows as if they were complete disks when using the \"Disk Device\" mount options. Once AIM has passed the contents of disk images mounted in these modes to Windows, the file system drivers you currently have installed take over and caveats like difficulty accessing protected files and folders may apply.\n\nWhat file systems does the Windows file system driver bypass mount option support?\n•   FAT 12/16/32\n•   NTFS\nExperimental support for:\n•   Btrfs\n•   Ext2/3/4 (except with 64 bit header fields used by some of the latest Linux distributions)\n•   ExFAT\n•   HFS+ \n•   SquashFs\n•   UDF\n•   XFS\n\nCan you describe some of the NTFS-related things exposed by the Windows file system driver bypass mount option as well as any NTFS-related limitations?\n•   NTFS metafiles (for example, $MFT, $LogFile, $UsnJrnl..$J)\n•   NTFS Alternate Data Streams (ADS) as files suffixed with their stream names alongside the \"normal\" files they are associated with\n•   NTFS streams in the [METADATA] folder at the root of each volume. You will find the entire volume's folder structure replicated here, and within each folder you will find the associated streams using the naming convention (STREAMNAME)..(STREAMTYPE). You can also find concatenated stream data for the entire volume at the root of the [METADATA] folder, using the naming convention [(STREAMNAME)]..[(STREAMTYPE)]. The streams currently exposed are $OBJECT_ID, $INDEX_ROOT, $INDEX_ALLOCATION, $EA, and $LOGGED_UTILITY_STREAM.\n•   Deleted files which have not been completely overwritten will be displayed in the [DELETED] folder at the root of each volume. Filenames will be appended (unless none of their clusters appear to have been reallocated, in which case they will remain as is) to identify what percentage of their clusters have apparently not been reallocated. If you see \"[0pct]\" appended to a filename, that indicates a very small number of clusters appear to have been reallocated and the percentage has been rounded down to 0. Also, orphans will be displayed within folders using the naming convention MFT-(#)_SEQ-(#). This functionality is based on the DiscUtils project and is best described as \"quick file and folder recovery.\" Please note that while browsing the contents of the [DELETED] folder you may encounter various kinds of corruption related to deleted files and folders (which will result in the error \"The disk structure is corrupted and unreadable.\") and that the contents of deleted files from SSDs (as opposed to HDDs) will often be empty.\n•   File slack, unallocated space, and volume slack are exposed at the root of each volume as [SLACK], [UNALLOCATED], and [VOLUME SLACK] respectively. Please note that the volume slack is related to space between the last cluster of the file system and the end of the volume.\n•   Support for some of the most recent NTFS features (such as CompactOS) are under development and not currently supported.\n\nWhat is the best mount mode for the purpose of an offline malware scan?\nGenerally speaking, the best mount mode for an offline malware scan is Windows file system driver bypass as it will provide the malware scanner with access to files that would not be readily accessible otherwise due to file system security. Please note, you may want to exclude certain “files” that AIM exposes in this mount mode which include [SLACK], [UNALLOCATED], [VOLUME SLACK], and possibly the contents of the [DELETED] folder.\n\nDoes Arsenal Image Mounter have command-line functionality?\nYes, please see readme_cli.txt for more details. In short, Arsenal Image Mounter CLI (aim_cli.exe) is a .NET 4.0 tool that provides most of Arsenal Image Mounter’s core functionality. The command “AIM_CLI /?” displays basic syntax for using Arsenal Image Mounter CLI. Arsenal Image Mounter CLI is provided with all versions of Arsenal Image Mounter. Arsenal Image Mounter Low Level (aim_ll.exe) is a tool that does not use .NET and provides more “low level” access to the Arsenal Image Mounter driver. The command “AIM_LL /?” displays basic syntax for using Arsenal Image Mounter Low Level. Arsenal Image Mounter Low Level is provided directly by Arsenal.\n\nHow can I share files, folders, and/or disks with virtual machines launched by Arsenal Image Mounter?\nWe normally prefer complete isolation of the virtual machines launched by AIM, but there are plenty of situations in which we need to share files, folders, and/or disks with virtual machines. Some methods of sharing include:\n•    Enabling guest services in AIM's Launch VM options and then enabling Enhanced Session Mode (by selecting \"Connect\" at the \"Display Configuration” dialog on Windows 8+) while the VM is booting, which will allow copy/paste between the host and virtual machine\n•    Enabling guest services and Enhanced Session Mode as above will also allow USB drives already attached to the host to be attached to the VM, by selecting “Show Options/Local Resources/Local devices and resources/More…” at the Enhanced Session dialog, then under “Drives” selecting which USB drives to attach to the virtual machine\n•    Using the Hyper-V Settings/SCSI Controller/Hard Drive/Add/Physical hard disk dropdown to add an offline disk to the VM once it has booted (a \"disk\" can be a real disk, a VHDX, or a RAM disk created by AIM), which will allow the disk to be used exclusively by the VM\n•    In Generation 1 VMs, using the Hyper-V Settings/IDE Controller/Physical CD/DVD drive dropdown to add a directory or archive mounted by AIM with CD/DVD-ROM emulation to the VM once it has booted, which will allow the disk to be used simultaneously (but read only!) by the host and VM (if you have more than one directory or archive mounted by AIM and would like to switch between them in the VM, use the aforementioned Physical CD/DVD drive dropdown)\n•    Using AIM's \"Attach to existing virtual machine\" feature which effectively replaces the \"Hyper-V Settings/SCSI Controller\" and \"Hyper-V Settings/IDE Controller\" methods mentioned above, to create more efficient workflow\n•    Using PowerShell Direct (if both the host and virtual machine are running Windows 10, 11, or Windows 2016) to run PowerShell commands such as Copy-Item and Enter-PSSession against a virtual machine, regardless of its network or remote management settings. Copy-Item is somewhat self explanatory and Enter-PSSession allows you to run commands within the virtual machine. See https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/powershell-direct for more information. Please note that PowerShell will ask for credentials of the account within the virtual machine that you are interested in, which you must provide as DOMAIN\\USER or COMPUTER\\USER. You do not need to enter a password if AIM has already performed Windows authentication bypass... but please note that even though you do not need to provide a password, PowerShell Direct still requires that the account originally had one. Here is sample syntax for Copy-Item first and then Enter-PSSession, each targeting a SANS Windows 10 workstation launched into a VM by AIM:\n\tCopy-Item -FromSession (New-PSSession -VMName AIM_base-rd01-cdrive.e01_5E3437D9) -Path “c:\\users\\tdungan\\documents\\demon core.pdf” -Destination c:\\users\\administrator\\desktop\n\tEnter-PSSession -VMName AIM_base-rd01-cdrive.e01_5E3437D9\n\nHow can I or my organization contribute to Arsenal Image Mounter?\nIf Arsenal Image Mounter has become a valuable part of your toolkit, please let your colleagues in digital forensics know. We would also appreciate knowing how you use AIM and if you have any suggestions for future versions. If you or your organization have used AIM source code, APIs, and/or executables in open-source or commercial projects, please make sure you are complying with our licensing requirements. Commercial licensing of AIM source code, APIs, and/or executables helps us offset the cost of continued development, both in terms of Free and Professional Mode functionality.\n\nWhat does \"Create removable disk device\" in the \"Mount Options\" screen do?\nThis function essentially emulates the attachment of a USB thumb drive. We have heard that it facilitates the mounting of disk images containing partitions rather than disks, even though Arsenal Image Mounter was initially designed to mount disks specifically. Characteristics (and limitations) of using this function include:\n•    Windows (prior to Windows 10 Build 1703) will only identify and use the first partition in the disk image, even if it contains more than one partition\n•    SAN policies such as requiring new devices to be mounted offline do not apply\n•    Drive letters are always assigned even if automatic drive letter assignment is turned off\n•    Windows identifies and uses file systems even for single-volume disk images that have no partition table\n•    Inability to interact with Volume Shadow Copies natively\n\nDoes Arsenal licensing require an Internet connection?\nYou only need an Internet connection in regard to Arsenal licensing when you initially activate your license code, once a year for a license validation, and when you extend/renew your license. If you cannot connect to the Internet, please see \"How can I activate an Arsenal license on an offline/air-gapped workstation?\" below.\n\nHow can I activate an Arsenal license on an offline/air-gapped workstation?\nIf you want your offline/air-gapped workstation properly licensed to run Arsenal Image Mounter and our other tools:\n1.) Open Arsenal Image Mounter and enter the license code you were given\n2.) Upon realizing that no Internet connection is available, Arsenal Image Mounter will save a “.LIC” file to your ProgramData\\ArsenalRecon folder\n3.) On a workstation with Internet access, go to our Offline Activation page at https://www.softworkz.com/offline/offline.aspx and upload the “.LIC” file\n4.) Finally, copy the CDM file you receive to the ProgramData\\ArsenalRecon folder on your offline/air-gapped workstation\nYour offline/air-gapped workstation is now ready to run all the Arsenal tools! Please note, if you provide your offline/air-gapped workstation with Internet access for some reason and then launch our tools, the Arsenal license type will be converted from offline to online.\n\nI purchased an Arsenal license extension/renewal, but how do I apply it?\nIf you have extended/renewed an existing Arsenal license and your forensic workstation has Internet access, but the next time you launch Arsenal Image Mounter you do not see updated subscription information on the Help/About screen..., you can try selecting the \"Update license\" button. If you have extended/renewed an existing Arsenal license and your forensic workstation is offline, you need to remove the Arsenal license file (.CDM file) from ProgramData\\ArsenalRecon and perform another offline activation.\n\nI purchased a new Arsenal license to replace an existing license with an active subscription, but how do I apply it?\nIf you would like to replace an existing Arsenal license with an active subscription, remove the Arsenal license file (.CDM file) from ProgramData\\ArsenalRecon, launch Arsenal Image Mounter, enter the new license code, and follow either the online (if your forensic workstation has Internet access) or offline/air-gapped activation process. \n\nHow can I mount and launch virtual machines from disk images containing BitLocker-protected volumes?\nWhen you use Arsenal Image Mounter to mount a disk image containing BitLocker-protected volumes, Windows will recognize those volumes and either ask to unlock them with a key (assuming they were in a locked state) or it will begin real-time decryption without requiring any user input (assuming they were in a disabled or suspended state.) There are a variety of ways in which \"BitLockered disk images\" (how Arsenal refers to disk images containing one or more BitLocker-protected volumes) can be launched into virtual machines. Here are some examples of workflows to launch BitLockered disk images into virtual machines:\n\nThis workflow is what we recommend if you would like maximum performance from the virtual machine:\n1.) Use AIM to mount the disk image containing one or more BitLocker-protected volumes in write-temporary mode\n2.) Use AIM's \"Fully decrypt BitLocker-protected volumes\" feature*\n3.) Use AIM’s Launch VM feature to launch a virtual machine\n4.) Run AIM Virtual Machine Tools by selecting the “Ease of Access” or \"Accessibility\" icon and use password bypass, etc. as desired\n\n* This feature turns BitLocker off - fully decrypting all the contents of the BitLocker-protected volume. This is a time-consuming process and you can check on the status of full BitLocker decryption by using \"manage-bde -status Volume Letter:\" at a command prompt. Unlocking (rather than fully decrypting) BitLocker only results in real-time decryption of the BitLocker-protected volume contents as necessary, rather than full decryption.\n\nThis workflow is what we recommend for fastest access to the virtual machine (as there is no wait for full decryption):\n1.) Use AIM to mount the disk image containing one or more BitLocker-protected volumes in write-temporary mode\n2.) Use AIM's \"Unlock BitLocker-protected volumes\" feature or Windows itself on your forensic workstation to unlock the BitLocker-protected volume(s)\n3.) Use AIM’s Launch VM feature to launch a virtual machine and select disable/suspend* BitLocker-protected volumes\n4.) Run AIM Virtual Machine Tools by selecting the “Ease of Access” or \"Accessibility\" icon and use password bypass, etc. as desired\n\n* By disable/suspend, we are referring to exposing the BitLockered volume's encryption key in the clear (the equivalent of \"manage-bde -protectors -disable (Volume Letter:)\"), turning off any volume protection. \n\nThis workflow we do not recommend, because AIM Virtual Machine Tools will not be injected and you will be on your own in terms of logging in to any Windows accounts:\n1.) Use AIM to mount the disk image containing one or more BitLockered-protected volumes in write-temporary mode\n2.) Do not unlock BitLocker\n3.) Use AIM’s Launch VM feature to launch a virtual machine (without allowing AIM to unlock and disable BitLocker protection)\n\nCan I use Arsenal Image Mounter to mount Volume Shadow Copies (VSCs) in Windows natively?\nYes, you can enable Arsenal Image Mounter's “Professional Mode” to access VSC mounting functionality and choose to mount the contents of VSCs three different ways - with the Windows NTFS driver, the DiscUtils NTFS driver, or as a complete disk (with the Windows NTFS driver). You can also leverage AIM’s \"Free Mode\" disk image mounting functionality along with other tools such as Eric Zimmerman's VSCMount at https://ericzimmerman.github.io/#!index.md or as described on David Cowen’s blog at http://www.hecfblog.com/2014/02/daily-blog-240-arsenal-image-mounter.html.\n\nCan you provide more detail about the Windows bugs identified by Maxim Suhanov and Arsenal which sometimes result in \"missing\" Volume Shadow Copies?\nMaxim Suhanov identified unexpected behavior from the volsnap driver when handling read-only mounted volumes on some insider builds of Windows 10 and a leaked version of Windows 11 - see https://dfir.ru/2021/06/28/shadow-copies-become-less-visible/#more-1212. Due to how Volume Shadow Copy functionality works, Windows actually needs write access to the volume for everything to work properly - even though it has not appeared this way through most builds of Windows 10. The volsnap driver in most Windows 10 builds (other than the recent Insider builds discussed by Maxim Suhanov) would encounter errors when mounting VSCs on volumes mounted read only, but left the symbolic links (such as \\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopyXXX) exposed anyway. In the Windows 10 insider builds discussed by Maxim Suhanov and recent Windows 11 builds, the volsnap driver will no longer expose those symbolic links. The workaround for this issue is to mount volumes containing VSCs in write-temporary mode, but Arsenal has identified a different bug in the volsnap driver which sometimes (i.e. non-deterministic behavior) results in \"missing\" VSCs even when volumes are mounted in write-temporary mode. This bug has been found only in recent Windows 11 builds. When this bug is triggered, VSCs will be deleted by Windows without user notification. Arsenal has reported this bug to Microsoft. The workaround is to re-mount the disk image containing Windows in write-temporary mode. AIM post v3.9.239 contains logic when Windows is mounted in either read only or write-temporary modes which compares a low-level VSC scan against VSCs identified by the volsnap driver (but please be aware this comparison will not be done if the Windows volume is encrypted), and a warning with a recommended solution will be displayed if a delta is found.\n\nHow can I release or attach my mouse from a virtual machine launched by AIM?\nYou can release your mouse from Hyper-V by using the keyboard shortcut CTRL-ALT-LEFT ARROW. In some cases you may find that clicking within the Hyper-V virtual machine does not immediately attach your mouse, but if you wait until the operating system within the virtual machine is ready for input (in other words, it's not busy!) you will then be able to attach your mouse. More keyboard shortcuts can be found at https://blogs.msdn.microsoft.com/virtual_pc_guy/2008/01/14/virtual-machine-connection-key-combinations-with-hyper-v.\n\nCan I use Arsenal Image Mounter to decrypt full-disk or volume encryption within disk images?\nYes, Arsenal Image Mounter is used frequently for this purpose. Generally speaking, you have two great options - use AIM to mount your disk image as a “real” disk and let full-disk or volume encryption software on your host proceed, or launch your disk image into a virtual machine to interact with either the full-disk encryption's limited OS or the volume encryption's native applications within the virtual machine. You can see screenshots from both of these options applied to a disk image containing Symantec Encryption Desktop (a/k/a PGP Desktop) at https://twitter.com/ArsenalRecon/status/1242094213929537540. If you are dealing with BitLocker, AIM also has BitLocker-related functionality to assist you.\n\nAre you having trouble booting decrypted BitLocker volumes?\nSee Adam Bridge’s excellent blog post on modifying an NTFS volume’s Volume Boot Record (VBR) using Arsenal Image Mounter’s “Write temporary” mode at https://www.contextis.com/resources/blog/making-ntfs-volume-mountable-tinkering-vbr/.\n\nHow can I fix AIM’s drop-down menus from flying out beyond the GUI’s borders?\nThis behavior may be related to Windows Presentation Framework and “handedness.” Your handedness setting can be found by hitting Windows key+R, then pasting in “shell:::{80F3F1D5-FECA-45F3-BC32-752C152E456E}”. If your handedness setting is “Right-handed” you may want to change it to “Left-handed”.\n\nI accidentally set the Hyper-V view so small that I can no longer access the \"View\" drop-down menu, how can I fix this?\nClose Hyper-V, change the \"ZoomLevel\" value within %UserProfile%\\AppData\\Roaming\\Microsoft\\Windows\\Hyper-V\\Client\\1.0\\vmconnect.config file to 100 (or at least something larger than it is currently set to.), and then launch another virtual machine.\n\nI have noticed that AIM-mounted (or attached) disks exhibit unusual behavior (e.g. inability to offline disks) on one forensic workstation, but not on others - what could be wrong?\nYour forensic workstation may be automatically encrypting (BitLocker protector-free encryption, a/k/a \"Clear Key Mode\") all newly-attached disks per Windows policy. One method to fix this behavior is to remove BitLocker from your forensic workstation's Windows volume, which will disable this problematic policy. You can then enable BitLocker again without this problematic policy interfering with newly-attached disks.\n\nWill using Hyper-V's \"Enhanced Session Mode\" cause any problems with Windows virtual machines?\nPotentially, yes. We do not recommend using Hyper-V's Enhanced Session Mode (which appears as a \"Display Configuration” dialog during the launch of virtual machines running Windows 8+ and essentially uses Remote Desktop to connect to the virtual machine) because unexpected policy issues may surface - for example, accounts may be prohibited from remote and password-less logons. If you are booting a virtual machine and see the Enhanced Session Mode dialog asking about screen resolution, just exit that dialog and you will be returned to direct console mode.\n\nWhy isn't Hyper-V running properly on bare metal even though I'm sure it's installed?\nIf you are sure Hyper-V has been installed, but when you run \"sc query HvService\" from a command prompt you are notified that it is not running, it's possible that there is an issue with boot configuration due to the presence of other virtualization platforms like VMware or Oracle VM VirtualBox. You may be able to resolve this issue by running \"bcdedit /set hypervisorlaunchtype auto\" at an administrative command prompt (which will result in Hyper-V starting at boot), but please note that you may need to reverse this action (\"bcdedit /set hypervisorlaunchtype off\") later to make sure your other virtualization platforms work as expected. \n\nCan I run Hyper-V within VMware or Hyper-V within Hyper-V?\nWe do not recommend nesting virtualization environments, but some of our customers are doing so successfully. You can find details on running Hyper-V within VMware and Hyper-V within Hyper-V in the Insights article at https://ArsenalRecon.com/insights/arsenal-image-mounter-and-virtual-machine-inception.\n\nWhy am I unable to see DPAPI-protected data within a virtual machine running Windows, even though I have an account's actual PIN?\nMicrosoft accounts (Microsoft (cloud) in AIM Virtual Machine Tools) used in combination with Windows Hello can be configured to require TPM and disable password logons. In the event you launch a virtual machine from a disk image containing a Microsoft account in this condition, you will be able to perform a Windows authentication bypass, but if Arsenal Image Mounter has not already made DPAPI bypass available in the Launch VM options you will not be able to unlock DPAPI to access protected data - even if you have the account’s actual PIN which was used on the original device.\n\nWhy am I unable to see DPAPI-protected data within a virtual machine running Windows, even though DPAPI bypass was available to me in the Launch VM Options screen?\nArsenal Image Mounter may be unable to determine prior to Windows booting whether a DPAPI bypass will work successfully when dealing with Microsoft accounts (Microsoft (cloud) in AIM Virtual Machine Tools) that use PIN authentication and have not also used (depending on various circumstances) password or picture-password authentication. If you encounter this scenario, AIM will deliver you to a Windows logon screen rather than the selected user’s Desktop.\n\nWhy is it taking so long to mount my disk image?\nIn some (rare) situations, you may find that it takes an hour or more to mount a disk image. We have found that the conditions are relatively extreme when this occurs - for example, when mounting disk images 10TB+ in size within encrypted volumes on external hard drives and consisting of thousands of segments.\n\nIs it possible to deploy Arsenal Image Mounter unattended?\nTo some extent, yes. We can provide customers with an installation package containing the Arsenal Image Mounter driver and the AIM CLI application, which can be installed silently depending on circumstances. While the installation will be silent in terms of Arsenal Image Mounter itself, it may not be silent in terms of Windows due to policy - for example, users may need to confirm that they trust drivers from Arsenal.\n\nIs there an Application Programming Interface (API)?\nYes – Arsenal Image Mounter provides both .NET and non-.NET APIs. You can find these APIs on our GitHub page at https://github.com/ArsenalRecon/Arsenal-Image-Mounter/tree/master/API.\n\nWhat programming languages have been used to build Arsenal Image Mounter?\nArsenal Image Mounter’s Storport miniport driver is written in C and its user mode API library is written in VB.NET, which facilitates easy integration with .NET 4.0 applications.\n\nWhere can I find the source code?\nArsenal Image Mounter source code can be found on GitHub at https://github.com/ArsenalRecon/Arsenal-Image-Mounter.\n\nHow can I uninstall Arsenal Image Mounter?\nIf you would like to completely uninstall Arsenal Image Mounter (perhaps you want to revert to an earlier version), go to Device Manager\\Storage controllers\\Arsenal Image Mounter, right-click and select \"Uninstall device\". Then, from an administrative command prompt:\n1.) [Optional] If you have the Windows Driver Kit (WDK) installed (or Visual Studio, or the Windows SDK), you can run \"devcon remove *phdskmnt\" (for example, C:\\Program Files (x86)\\Windows Kits\\10\\Tools\\x64\\devcon remove *phdskmnt) instead of using Device Manager\n2.) sc delete phdskmnt\n3.) sc delete aimwrfltr\n4.) [Optional] sc stop vhdaccess\n5.) [Optional] sc delete vhdaccess\n6.) [Optional] sc stop awealloc\n7.) [Optional] sc delete awealloc\n8.) [Optional] sc stop dokan1\n9.) [Optional] sc delete dokan1\n10.) Delete phdskmnt.sys and aimwrfltr.sys from C:\\Windows\\system32\\drivers\n11.) [Optional] Delete vhdaccess.sys, awealloc.sys and dokan1.sys from C:\\Windows\\system32\\drivers\n12.) Delete the Arsenal Image Mounter executables, libraries, and documentation from where you placed them\n\nClarifications regarding terminology:\nThe phrases \"Removing disk\" and \"Unmounting disk image\" essentially refer to the same thing when you see them in dialog boxes, documentation, and blog posts related to Arsenal Image Mounter.\n\nUse and License\nWe chose a dual-license for Arsenal Image Mounter (more specifically, Arsenal Image Mounter’s source code, APIs, and executables) to allow for royalty-free use in open source projects, but require financial support from commercial projects.\n\nArsenal Consulting, Inc. (d/b/a Arsenal Recon) retains the copyright to Arsenal Image Mounter, including the Arsenal Image Mounter source code, APIs, and executables, being made available under terms of the Affero General Public License v3. Arsenal Image Mounter source code, APIs, and executables may be used in projects that are licensed so as to be compatible with AGPL v3. If your project is not licensed under an AGPL v3 compatible license and you would like to use Arsenal Image Mounter source code, APIs, and/or executables, contact us (sales@ArsenalRecon.com) to obtain alternative licensing.\n\nContributors to Arsenal Image Mounter must sign the Arsenal Contributor Agreement (“ACA”). The ACA gives Arsenal and the contributor joint copyright interests in the source code.\n\n"
  },
  {
    "path": "tools/Arsenal-Image-Mounter-v3.10.257/readme_cli.txt",
    "content": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounter offers two command-line interface executables:\n\nArsenal Image Mounter CLI (a/k/a AIM CLI, aim_cli.exe) is a .NET tool that provides an integrated command line interface to Arsenal Image Mounter's virtual SCSI miniport driver. Most of Arsenal Image Mounter’s core functionality is available with AIM CLI. The command “AIM_CLI --?” displays basic syntax for using AIM CLI. AIM CLI mounts disk images in read-only mode by default. AIM CLI is provided within the Arsenal Image Mounter download.\n\nArsenal Image Mounter Low Level (a/k/a AIM LL, aim_ll.exe) is a tool that does not use .NET and provides more “low level” access to the Arsenal Image Mounter driver. The command “AIM_LL --?” displays basic syntax for using AIM LL. AIM LL mounts disk images in write-original mode by default, to maintain compatibility with existing scripts. AIM LL is largely deprecated and is only available directly from Arsenal.\n\nPlease note: \n•   Arsenal Image Mounter CLI and Low Level should normally be run with administrative privileges, but converting disk images from one format to another without mounting (and without using physical disks as targets) and calculating checksums can be done without administrative privileges.\n•   If you would like to use AIM CLI or LL executables to interact with EnCase (E01 and Ex01), AFF4, or qcow/qcow2 disk images, you must make the Libewf (libewf.dll), LibAFF4 (libaff4.dll), and qcow/qcow2 (libqcow.dll) libraries available in the expected (/lib/x64) or same folder as the AIM CLI or LL executable.\n\nAlso note that AIM GUI actively onlines partitions within mounted/attached disks, but AIM CLI does not. Mounting/attaching a disk with AIM CLI is similar to simply attaching a physical disk to Windows without any extra logic, so (for example) Windows policy on the forensic workstation or file system errors within the mounted/attached disk may result (at least initially) in offline partitions and a lack of drive letter assignments.\n\nParticular examples of Arsenal Image Mounter CLI syntax:\n\n#mount an E01 forensic disk image with the write-temporary mount option and fake disk signature, then automatically delete the differencing file\naim_cli.exe --mount --fakesig --filename=C:\\path\\Win10Disk.E01 --provider=libewf --writeoverlay=C:\\path\\Win10Disk.E01.diff --autodelete\n\n#mount a dd raw disk image with the write-original mount option\naim_cli.exe --mount --writable --filename=C:\\path\\Win10Disk.dd\n\n#mount a VMDK virtual machine disk image with the read-only mount option\naim_cli.exe --mount --readonly --filename=C:\\path\\Win10Disk.vmdk --provider=DiscUtils\n\n#convert an E01 forensic disk image to a new dd raw disk image, without mounting\naim_cli.exe --filename=Win10Disk.E01 --provider=LibEWF --convert=rawconversion.dd\n\n#save an already mounted E01 forensic disk image (using disk id from AIM) to a dd raw disk image\naim_cli.exe --device=000200 --saveas=rawoutput.dd\n\n#save an already mounted E01 forensic disk image (using disk device name from AIM) to a dd raw disk image\naim_cli.exe --device=\\\\?\\physicaldrive4 --saveas=rawoutput.dd\n\n#restore an E01 forensic disk image to an actual physical disk\naim_cli.exe --filename=Win10Disk.E01 --provider=LibEwf --convert=\\\\?\\PhysicalDrive4\n\nDetailed Arsenal Image Mounter CLI syntax:\n\n#mount a raw/forensic/virtual machine disk image as a \"real\" disk\naim_cli.exe --mount[:removable|:cdrom] [--buffersize=bytes] [--readonly] [--writable] [--fakesig] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--writeoverlay=differencingimagefile] [--autodelete] [--background]\n\n#start shared memory service mode, for mounting from other applications\naim_cli.exe --name=objectname [--buffersize=bytes] [--readonly] [--writable] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--background]\n\n#start TCP/IP service mode, for mounting from other computers\naim_cli.exe [--ipaddress=listenaddress] --port=tcpport [--readonly] [--writable] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--background]\n\n#convert a disk image without mounting\naim_cli.exe [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None --convert=outputimagefilename [--variant=fixed|dynamic] [--background]\n\n#calculate MD5, SHA1, or SHA256 checksum over disk image contents without mounting (all three caculated if a specific checksum is not specified)\naim_cli.exe --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None --checksum=[MD5|SHA1|SHA256]\n\n#save a new disk image after mounting\naim_cli.exe --device=sixdigitdevicenumber|\\\\?\\physicaldriveN --saveas=outputimagefilename [--variant=fixed|dynamic] [--background]\n\n#dismount a mounted device\naim_cli.exe --dismount[=sixdigitdevicenumber|\\\\?\\physicaldriveN] [--force]\n\n#restore a disk image to an actual physical disk\naim_cli.exe --filename=imagefilename [--fakembr] --provider=DiscUtils|LibEwf|LibAFF4|MultiPartRaw|None --convert=\\\\?\\PhysicalDriveN [--background]\n\nAdditional information regarding Arsenal Image Mounter CLI switches:\n\nThe --background switch will re-launch AIM CLI in a new process, detach from the current console window, and continue running in the background.\n\nWhen the --force switch is used in combination with --dismount, the specified device is dismounted even if it may be in use.\n\nThe --autodelete switch will automatically delete the differencing file after the disk image is dismounted.\n\nWhen converting or saving \"physical\" objects (whether mounted or not), output type for disk images can be raw (.raw, .dd, .img, .ima, .bin), forensic (.e01), virtual machine (.vhd, .vhdx, .vdi, .vmdk), or dmg (.dmg). AIM CLI selects output type based on the outputimagefilename file extension. For virtual machine disk image formats, the optional --variant switch can be used to specify either fixed or dynamically expanding formats - the default is dynamic. This function can also be used to save virtually mounted objects (such as archives or directories mounted as CD/DVD-ROMs) to raw CD/DVD images, using the extensions .iso and .bin.\n\nUse and License\nWe chose a dual-license for Arsenal Image Mounter (more specifically, Arsenal Image Mounter’s source code, APIs, and executables) to allow for royalty-free use in open source projects, but require financial support from commercial projects.\n\nArsenal Consulting, Inc. (d/b/a Arsenal Recon) retains the copyright to Arsenal Image Mounter, including the Arsenal Image Mounter source code, APIs, and executables, being made available under terms of the Affero General Public License v3. Arsenal Image Mounter source code, APIs, and executables may be used in projects that are licensed so as to be compatible with AGPL v3. If your project is not licensed under an AGPL v3 compatible license and you would like to use Arsenal Image Mounter source code, APIs, and/or executables, contact us (sales@ArsenalRecon.com) to obtain alternative licensing.\n\nContributors to Arsenal Image Mounter must sign the Arsenal Contributor Agreement (“ACA”). The ACA gives Arsenal and the contributor joint copyright interests in the source code.\n\n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/NEWS.txt",
    "content": "---------------- VERSION 4.12.1 --------------\nC/C++:\n- Bug fixes from Luis Nassif and Joachim Metz\n- Added check to stop for very large folders to prevent memory exhausion\n\nJava:\n- Added File Repository concept for files to be stored in another location\n- Schema updated to 9.4\n- Fixed OS Account merge bug and now fire events when accounts are merged\n\n\n---------------- VERSION 4.12.0 --------------\n- There was a 1-year gap since 4.11.1 and the git log has 441 commits in that timeframe. \n- Many for small fixes.  \n- This set of release notes is much more of an overview than other releases\n\nWhat's New:\n- LVM Support (non-Windows) from Joachim Metz\n- Logical File System support (a folder structure is parsed by TSK libraries) from Ann Priestman (Basis)\n\nWhat's Changed:\n- Lots of bug fixes from the Basis team and Joachim Metz\n- Additional fixes from Eran-YT, msuhanov, Joel Uckelman, Aleks L, dschoemantruter\n- General themes of C/C++ bounds checks and Java improvements to OS Accounts, Ingest jobs, CaseDbAccessManager, and much more.\n\n\n\n---------------- VERSION 4.11.1 --------------\n\nC/C++:\n- Several fixes from Joachim Metz\n- NTFS Decompression bug fix from Kim Stone and Joel Uckelman\n\nJava:\n- Fixed connection leak when making OS Accounts in bridge\n- OsAccount updates for instance types and special Windows SIDs\n- Fixed issue with duplicate value in Japanese timeline translation\n\n\n---------------- VERSION 4.11.0 --------------\nC/C++:\n- Added checks at various layers to detect encrypted file systems and disks to give more useful error messages.\n- Added checks to detect file formats that are not supported (such as AD1, ZIP, etc.) to give more useful error messages.\n- Added tsk_imageinfo tool that detects if an image is supported by TSK and if it is encrypted.\n- Add numerous bound checks from Joachim Metz.\n- Clarified licenses as pointed out by Joachim Metz.\n\nJava:\n- Updated from Schema 8.6 to 9.1.\n- Added tables and classes for OS Accounts and Realms (Domains).\n- Added tables and classes for Host Addresses (IP, MAC, etc.).\n- Added tables and classes for Analysis Results vs Data Artifacts by adding onto BlackboardArtifacts.\n- Added tables and classes for Host and Person to make it easier to group data sources.\n- Added static types for standard artifact types.\n- Added File Attribute table to allow custom information to be stored for each file.\n- Made ordering of getting lock and connection consistent.\n- Made the findFile methods more efficient by using extension (which is indexed).\n\n\n\n---------------- VERSION 4.10.2 --------------\nC/C++\n- Added support for Ext4 inline data\n\nJava\n- New Blackboard Artifacts for ALEAPP/ILEAPP, Yara, Geo Area, etc.\n- Upgraded to PostgreSQL JDBC Driver 42.2.18\n- Added SHA256 to files table in DB and added utility calculation methods.\n- Changed TimelineManager to make events for any artifact with a time stamp\n- Added Japanese translations\n- Fixed sychronization bug in getUniquePath\n\n\n---------------- VERSION 4.10.1 --------------\nC/C++:\n- Changed Windows build to use Nuget for libewf, libvmdk, libvhdi.\n- Fixed compiler warnings \n- Clarrified licenses and added Apache license to distribution\n- Improved error handling for out of memory issues\n- Rejistry++ memory leak fixes\n\nJava:\n- Localized for Japanese\n\n---------------- VERSION 4.10.0 --------------\nC/C++:\n- Removed PostgreSQL code (that was used only by Java code)\n- Added Java callback support so that database inserts are done in Java.\n\nJava: \n- Added methods and callbacks as required to allow database population to happen in Java instead of C/C++.\n- Added support to allow Autopsy streaming ingest where files are added in batches. \n- Added TaggingManager class and concept of a TagSet to support ProjectVic categories. \n- Fixed changes to normalization and validation of emails and phone numbers.\n- Added a CASE/UCO JAR file that creates JSON-LD based on TSK objects.\n\n\n\n---------------- VERSION 4.9.0 --------------\nC/C++\n- Removed framework project.  Use Autopsy instead if you need an analysis framework. \n- Various fixes from Google-based fuzzing.\n- Ensure all reads (even big ones) are sector aligned when reading from Windows device.\n- Ensure all command line tools support new pool command line arguments. \n- Create virtual files for APFS unallocated space\n- HFS fix to display type\n\nJava:\n- More artifact helper methods\n- More artifacts and attributes for drones and GPS coordinates\n- Updated TimelineManager to insert GPS artifacts into events table\n\n\n---------------- VERSION 4.8.0 --------------\nC/C++\n- Pool layer was added to support APFS. NOTE: API is likely to change. \n- Limited APFS support added in libtsk and some of the command line tools. \n-- Encryption support is not complete. \n-- Blackbag Technologies submitted the initial PR. Basis Technology\n  did some minor refactoring.\n- Refactoring and minor fixes to logical imager\n- Various bug fixes from Google fuzzing efforts and Jonathan B from Afarsec\n- Fixed infinite NTFS loop from cyclical attribute lists.  Reported by X.  \n- File system bug fixes from uckelman-sf on github\n\nDatabase: \n- DB schema was updated to support pools \n- Added concept of JSON in Blackboard Attributes\n- Schema supports cascading deletes to enable data source deletion\n\nJava:\n- Added Pool class and associated infrastructure\n- Added methods to support deleting data sources from database\n- Removed JavaFX as a dependency by refactoring the recently\n  introduced timeline filtering classes.\n\n- Added attachment support to the blackboard helper package. \n\n\n\n\n---------------- VERSION 4.7.0 --------------\nC/C++:\n- DB schema was expanded to store tsk_events and related tables.\nTime-based data is automatically added when files and artifacts are\ncreated.  Used by Autopsy timeline.\n- Logical Imager can save files as individual files instead of in\nVHD (saves space).\n- Logical imager produces log of results\n- Logical Imager refactor\n- Removed PRIuOFF and other macros that caused problems with\nsigned/unsigned printing. For example, TSK_OFF_T is a signed value\nand PRIuOFF would cause problems as it printed a negative number\nas a big positive number.\n\n\nJava\n- Travis and Debian package use OpenJDK instead of OracleJDK\n- New Blackboard Helper packages (blackboardutils) to make it easier\nto make artifacts.\n- Blackboard scope was expanded, including the new postArtifact() method\nthat adds event data to database and broadcasts an event to listeners.\n- SleuthkitCase now has an EventBus for database-related events.\n- New TimelineManager and associated filter classes to support new events \ntable\n\n\n\n---------------- VERSION 4.6.7 --------------\nC/C++ Code:\n- First release of new logical imager tool\n- VHD image writer fixes for out of space scenarios\n\nJava:\n- Expand Communications Manager API\n- Performance improvement for SleuthkitCase.addLocalFile()\n\n\n\n---------------- VERSION 4.6.6 --------------\n\nC/C++ Code:\n- Acquisition deteails are set in DB for E01 files\n- Fix NTFS decompression issue (from Joe Sylve)\n- Image reading fix when cache fails (Joe Sylve)\n- Fix HFS+ issue with large catalog files (Joe Sylve) \n- Fix free memory issue in srch_strings (Derrick Karpo)\n\nJava:\n- Fix so that local files can be relative\n- More Blackboard artifacts and attributes for web data\n- Added methods to CaseDbManager to enable checking for and modifying tables.\n- APIs to get and set acquisition details\n- Added methods to add volume and file systems to database\n- Added method to add LayoutFile for allocated files\n- Changed handling of JNI handles to better support multiple cases\n\n\n---------------- VERSION 4.6.5 --------------\nC/C++ Code:\n- HFS boundary check fix\n- New fields for hash values and acquisition details in case database\n- Store \"created schema version\" in case database\n\nJava Code:\n- New artifacts and attributes defined\n- Fixed bug in SleuthkitCase.getContentById() for data sources\n- Fixed bug in LayoutFile.read() that could allow reading past end offile\n\n\n---------------- VERSION 4.6.4 --------------\nJava Code:\n- Increase max statements in database to prevent errors under load\n- Have a max timeout for SQLite retries\n\n---------------- VERSION 4.6.3 --------------\nC/C++ Code:\n- Hashdb bug fixes for corrupt indexes and 0 hashes\n- New code for testing power of number in ExtX code\n\nJava Code: \n- New class that allows generic database access\n- New methods that check for duplicate artifacts\n- Added caches for frequently used content \n\nDatabase Schema: \n- Added Examiner table \n- Tags are now associated with Examiners\n- Changed parent_path for logical files to be consistent with FS files.\n\n\n---------------- VERSION 4.6.2 --------------\nC/C++ Code:\n- Various compiler warning fixes\n- Added small delay into image writer to not starve other threads\n\nJava: \n- Added more locking to ensure that handles were not closed while other threads were using them. \n- Added APIs to support more queries by data source\n- Added memory-based caching when detecting if an object has children or not.\n\n\n---------------- VERSION 4.6.1 --------------\nC/C++ Code:\n- Lots of bounds checking fixes from Google's fuzzing tests.  Thanks Google.\n- Cleanup and fixes from uckelman-sf and others\n- PostgreSQL, libvhdi, & libvmdk are supported for Linux / OS X\n- Fixed display of NTFS GUID in istat - report from Eric Zimmerman. \n- NTFS istat shows details about all FILE_NAME attributes, not just the first.  report from Eric Zimmerman.\n\nJava:\n- Reports can be URLs\n- Reports are Content\n- Added APIs for graph view of communications\n- JNI library is extracted to name with user name in it to avoid conflicts\n\nDatabase:\n- Version upgraded from to 8.0 because Reports are now Content\n\n\n---------------- VERSION 4.6.0 --------------\nNew Features\n- New Communications related Java classes and database tables.\n- Java build updates for Autopsy Linux build\n- Blackboard artifacts are now Content objects in Java and part of tsk_objects table in database.\n- Increased cache sizes.\n- Lots of bounds checking fixes from Google's fuzzing tests.  Thanks Google.\n- HFS fix from uckelman-sf.\n\n\n---------------- VERSION 4.5.0 --------------\nNew Features:\n- Support for LZVN compressed HFS files (from Joel Uckelman)\n- Use sector size from E01 (helps with 4k sector sizes)\n- More specific version number of DB schema\n- New Local Directory type in DB to differentiate with Virtual Directories\n- All blackboard artifacts in DB are now 'content'.  Attachments can now\n  be children of their parent message.\n- Added extension as a column in tsk_files table. \n\nBug Fixes:\n- Faster resolving of HFS hard links\n- Lots of fixes from Google Fuzzing efforts.\n\n\n---------------- VERSION 4.4.2 --------------\nNew Features:\n- usnjls tool for NTFS USN log (from noxdafox)\n- Added index to mime type column in DB\n- Use local SQLite3 if it exists (from uckelman-sf)\n- Blackboard Artifacts have a shortDescription metho\n\nBug Fixes:\n- Fix for highest HFS+ inum lookup (from uckelman-sf)\n- Fix ISO9660 crash\n- various performance fixes and added thread safety checks\n\n\n---------------- VERSION 4.4.1 --------------\n- New Features:\n-- Can create a sparse VHD file when reading a local drive with new\n   IMAGE_WRITER structure. Currently being used by Autopsy, but no TSK\n   command line tools.\n\n- Bug fixes:\n-- Lots of cleanup and fixes. Including:\n-- memory leaks\n-- UTF8 and UTF16 cleanup \n-- Missing NTFS files (in fairly rare cases)\n-- Really long folder structures and database inserts\n\n---------------- VERSION 4.4.0 --------------\n- Compiling in Windows now uses Visual Studio 2015\n- tsk_loaddb now adds new files for slack space and JNI was upgraded\n  accordingly.\n\n---------------- VERSION 4.3.1 --------------\n- NTFS works on 4k sectors\n- Added support in Java to store local files in encoded form (XORed)\n- Added Java Account object into datamodel\n- Added notion of a review status to blackboard artifacts\n- Upgraded version of PostgreSQL\n- Various minor bug fixes\n\n\n---------------- VERSION 4.3.0 --------------\n- PostgreSQL support (Windows only)\n- New Release_ NoLibs Visual Studio target\n- Support for virtual machine formats via libvmdk and libvhdi (Windows only)\n- Schema updates (data sources table, mime type, attributes store type)\n- tsk_img_open can take externally created TSK_IMG_INFO\n- Various minor bug fixes\n\n\n---------------- VERSION 4.2.0 --------------\n- ExFAT support added\n- New database schema\n- New Sqlite hash database\n- Various bug fixes\n- NTFS pays more attention to sequence and loads metadata only \n  if it matches. \n- Added secondary hash database index \n\n\n\n---------------- VERSION 4.1.3 --------------\n- fixed bug that could crash UFS/ExtX in inode_lookup.\n- More bounds checking in ISO9660 code \n- Image layer bounds checking\n- Update version of SQLITE-JDBC\n- changed how java loads navite libraries\n- Config file for YAFFS2 spare area\n- New method in image layer to return names\n- Yaffs2 cleanup.\n- Escape all strings in SQLite database\n- SQlite code uses NTTFS sequence number to match parent IDs\n\n\n---------------- VERSION 4.1.2 --------------\nCore:\n- Fixed more visual studio projects to work on 64-bit\n- TskAutoDB considers not finding a VS/FS a critical error.\n\nJava: \n- added method to Image to perform sanity check on image sizes.\n\nfiwalk:\n- Fixed compile error on Linux etc.\n\n\n\n---------------- VERSION 4.1.1 --------------\nCore: \n- Added FILE_SHARE_WRITE to all windows open calls.\n- removed unused methods in CRC code that caused compile errors.\n- Added NTFS FNAME times to time2 struct in TSK_FS_META to make them \n  easier to access -- should have done this a long time ago!\n- fls -m and tsk_gettimes output NTFS FNAME times to output for timelines.\n- hfind with EnCase hashsets works when DB is specified (and not only index)\n- TskAuto now goes into UNALLOC partitions by default too. \n- Added support to automatically find all Cellebrite raw dump files given\n  the name of the first image. \n- Added 64-bit windows targets to VisualStudio files.\n- Added NTFS sequence to parent address in directory and directory itself.\n- Updated SQLite code to use sequence when finding parent object ID.\n\nJava:\n- Java bindings JAR files now have native libraries in them. \n- Logical files are added with a transaction \n\n\n---------------- VERSION 4.1.0 --------------\nCore:\n- Added YAFFS2 support (patch from viaForensics).\n- Added Ext4 support (patch from kfairbanks)\n- changed all include paths to be 'tsk' instead of 'tsk3'\n-- IMPORTANT FOR ALL DEVELOPERS!\n \nFramework:\n- Added Linux and MAC support.\n- Added L01 support.\n- Added APIs to find files by name, path and extension.\n- Removed deprecated TskFile::getAttributes methods.\n- moved code around for AutoBuild tool support.\n\nJava Bindings:\n- added DerivedFile datamodel support\n- added a public method to Content to add ability to close() its tsk handle before the object is gc'd\n- added faster skip() and random seek support to ReadContentInputStream\n- refactored datamodel by pushing common methods up to AbstractFile\n- fixed minor memory leaks \n- improved regression testing framework for java bindings datamodel\n\n\n---------------- VERSION 4.0.2 --------------\nCore: \nNew Features:\n- Added fiwalk tool from Simson.  Not supported in Visual Studio yet.\n\nBug Fixes:\n- Fixed fcat to work on NTFS files (still doesn't support ADS though). \n- Fixed HFS+ support in tsk_loaddb / SQLite -- root directory was not added.\n- NTFS code now looks at all MFT entries when listing directory contents. It used to only look at unallocated entries for orphan files. This fixes an image that had allocated files missing from the directory b-tree.\n- NTFS code uses sequence number when searching MFT entries for all files. \n- Libewf detection code change to support v2 API more reliably (ID: 3596212). \n- NTFS $SII code could crash in rare cases if $SDS was multiple of block size. \n\nFramework:\n- Added new API to TskImgDB that returns the base name of an image.\n- Numerous performance improvements to framework.\n- Removed requirement in framework to specify module extension in pipeline configuration file.\n- Added blackboard artifacts to represent both operating system and network service user accounts.\n\nJava Bindings:\n- added more APIs to find files by name, path and where clause\n- added API to get currently processed dir when image is being added,\n- added API to return specific types of children of image, volume system, volume, file system.\n- moved more common methods up to Content interface\n- deprecated context of blackboard attributes, \n- deprecated SleuthkitCase.runQuery() and SleuthkitCase.closeRunQuery() \n- fixed ReadContentInputStream bugs  (ignoring offset into a buffer, implementing available() )\n- methods that are lazy loading are now thread safe\n- Hash class is now thread-safe\n- use more PreparedStatements to improve performance\n- changed source level from java 1.6 to 1.7\n- Throw exceptions from C++ side better\n\n\n---------------- VERSION 4.0.1 --------------\nNew Features:\n- Can open raw Windows devices with write mode sharing.\n- More DOS partition types are displayed.\n- Added fcat tool that takes in file name and exports content (equivalent to using ifind and icat together).\n- Added new API to TskImgDB that returns hash value associated with carved files.\n- performance improvements with FAT code (maps and dir_add)\n- performance improvements with NTFS code (maps)\n- added AONLY flag to block_walk\n- Updated blkls and blkcalc to use AONLY flag -- MUCH faster. \n\nBug Fixes:\n- Fixed mactime issue where it could choose the wrong timezone that did\n  not follow daylight savings times. \n- Fixed file size of alternate data streams in framework.\n- Incorporated memory leak fixes and raw device fixes from ADF Solutions.\n\n\n\n---------------- VERSION 4.0.0 --------------\nNew Features:\n- Added multithreaded support\n- Added C++ wrapper classes\n- Added JNI bindings / Java data model classes\n- 3314047: Added utf8-specific versions of 'toid' methods for img,vs,fs types\n- 3184429: More consistent printing of unset times (all zerso instead of 1970)\n- New database design that allows for multiple images in the same database \n- GPT volume system tries other sector sizes if first attempt fails.\n- Added hash calculation and lookup to AutoDB and JNI.\n- Upgraded SQLite to 3.7.9. \n- Added Framework in (windows-only)\n- EnCase hash support\n- Libewf v2 support (it is now non-beta)\n- First file in a raw split or E01 can be specified and the rest of the files\n  are found. \n- mactime displays times as 0 if the time is not set (isntead of 1970)\n- Changed behavior of 'mactime -y' to use ISO8601 format. \n- Updated HFS+ code from ATC-NY. \n- FAT orphan file improvements to reduce false positives. \n- TskAuto better reports errors. \n- Upgrade build projects from Visual Studio 2008 to 2010.\n\nBug Fixes:\n- Relaxed checking when conflict exists between DOS and GPT partitions.\nHad a Mac image that was failing to resolve which partition table\nto use. \n\n---------------- VERSION 3.2.3 --------------\nNew Features:\n- new TskAuto method (handleNotification()) that gets verbose messages that allow for debugging when the class makes decisions.\n- DOS partitions are loaded even if an extended partition fails to load\n- new TskAuto::findFilesInFs(TSK_FS_INFO *) method\n- Need to only specify first E01 file and the rest are found\n- Changed docs license to non-commercial\n- Unicode conversion routines fix invalid UTF-16 text during conversion\n- Added '-d' to tsk_recover to specify directory to recover\n\n\nBug Fixes:\n- Added check to fatfs_open to compare first sectors of FAT if we used backup boot sector and verify it is FAT32.\n- More checks to make sure that FAT short names are valid ASCII\n- 3406523: Mactime size sanity check\n- 3393960: hfind reading of Windows input file\n- 3316603: Error reading last blocks of RAW CD images\n- Fixed bugs in how directories and files were detected in TskAuto\n\n\n\n---------------- VERSION 3.2.2 --------------\nBug Fixes\n- 3213886: ISO9660 directory hole not advancing\n- 3173095 contd: Updated checks so that tougher FAT checks are\napplied to deleted directories.\n- 3303678: Image type in Sqlite DB is now not always 0\n- 3303679: Deleted FAT files have more name cleanup in short names\n\nNew Features:\n- 3213888: RAW CD format\n- Auto class accepts TSK_IMG_INFO as argument\n- Copies of split image file names are stored in TSK so that the caller can free them before TSK_IMG_INFO is freed.\n\n---------------- VERSION 3.2.1 --------------\nBug Fixes\n- 3108272: fls arguments for -d and -u\n- 3105539: compile error issues because of SQlite and pthreads\n- 3173095: missing FAT files because of invalid dates. \n- 3184419: mingew compile errors.\n- 3191391: surround file name in quotes in mactime -d csv output\n\nNew Features:\n- A single dummy entry is added to the SQlite DB if no volume exists\nso that all programs can assume that there will be at least one\nvolume in the table.\n- 3184455: allow srcdir != builddir\n\n---------------- VERSION 3.2.0 --------------\nBug Fixes\n- 3043092: Minor logic errors with ifind code. \n- FAT performance fix when looking for parent directories\n  in $OrphanFiles. \n- 3052302: Crash on NTFS/UFS detection test because of\n  corrupt data -- tsk_malloc error. \n- 3088447: Error adding attribute because of run collision.  \n  Solved by assigning unique IDs.\n\nNew Features:\n- 3012324: Name mangling moved out of library into outer tools\n  so that they can see control characters if they want to.  Patch\n  by Anthony Lawrence. \n- 2993806: ENUM values have a specified NONE value if you don't\n  want to specify any special flags. Patch by Anthony Lawrence.\n- 3026989: Add -e and -s flags to img_cat.  patch by Simson Garfinkel. \n- 2941805: Add case sensitive flag to fsstat in HFS.  Patch by Rob Joyce. \n- 3017764: Changed how default NTFS $DATA attribute was named.  Now it \n  has no name, while it previously had a fake name of \"$Data\". \n- New TskAuto class.\n- New tsk_loaddb, tsk_recover, tsk_comparedir, and tsk_gettimes tools. \n\n---------------- VERSION 3.1.3 --------------\nBug Fixes\n- 3006733: FAT directory listings were slow because the inner\ncode was not stopping when it found the parent directory.\n- Adjusted sanity / testing code on FAT directory entries to allow\nnon-ascii in extensions and reject entries with lots of 0s.\n- 3023606: Ext2 / ffs corrupted file names. \n- Applied NTFS SID fixes from Mandiant. \n- ntfs_load_secure() memory leak patch from Michael Cohen\n\n---------------- VERSION 3.1.2 --------------\nBug Fixes\n- 2982426: FAT directory listings were slow because the entire\nimage was being scanned for parent directory information. \n- 2982965: fs_attr length bug fix.\n- 2988619: mmls -B display error. \n- 2988330: ntfs SII cluster size increment bug\n- 2991487: Zeroed content in NTFS files that were not fully initialized.\n- 2993767: Slow FAT listings of OrphanFiles because hunt for parent\ndirectory resulted in many searches for OrphanFiles.  Added cache\nof OrphanFiles.\n- 2999567: ifind was not stopping after first hit.\n- 2993804: read past end of file did not always return -1.\n\n---------------- VERSION 3.1.1 --------------\n\nBug Fixes\n- 2954703: ISO9660 missing files because duplicate files\nhad same starting block. \n- 2954707: ISO9660 missing some files with zero length and\nduplicate starting block. Also changed behavior of how\nmultiple volume descriptors are processed. \n- 2955898: Orphan files not found if no deleted file names exist. \n- 2955899: NTFS internal setting of USED flag. \n- 2972721: Sorter fails with hash lookup if '-l' is given. \n- 2941813: Reverse HFS case sensitive flags (internal fix only)\n- 2954448: Debian package typo fixes, etc.\n- 2975245: sorter ignores realloc entries to reduce misleading mismatch entries and duplicate entries. \n\n\n---------------- VERSION 3.1.0 --------------\n\nNew Features and Changes\n- 2206285: HFS+ can now be read.  Lots of tracker items about this.\nThanks to Rob Joyce and ATC-NY for many of the patches and reports.\n- 2677069: DOS Safety Partitions in GPT Volume Systems are better\ndetected instead of reporting multiple VSs.\n- Windows executables can be build in Visual Studio w/out needing\nother image format libraries.\n- 2367426: Uninitialized file space is shown if slack space is\nrequested.\n- 2677107 All image formats supported by AFFLIB can be accessed by\nspecifying the \"afflib\" type.\n- 2206265: sigfind can now process non-raw files. \n- 2206331: Indirect block addresses are now available in the library\nand command line tools.  They are stored in a different attribute.\n- Removed 'docs' files and moved them to the wiki.\n- Removed disk_stat and disk_sreset because they were out of date\nand hdparm now has the same functionality.\n- 2874854: Image layer tools now support non-512 byte device sector\nsizes.  Users can specify sector size using the -b argument to the\ncommand line tools. This has several consequences: \n-- 'mmls -b' is now 'mmls -B'.  Similarly with istat -b.\n-- Changed command line format for '-o' so that sector size is\nspecified only via -b and not using '-o 62@4096'.\n- 2874852: Sanity checking on partition table entires is relaxed\nand only first couple of partitions are checked to make sure that\nthey can fit into the image.\n- 2895607: NTFS SID data is available in the library and 'istat'.\n- 2206341: AFF encrypted images now give more proper error message\nif password is not given.\n- 2351426: mactime is now distributed with Windows execs. \n\n\nDeveloper-level Changes\n- Abstracted name comparison to file system-specific function.\n- Added support in mactime to read body files with comment lines.\n- 2596153: Changed img_open arguments, similar to getopt().\n- 2797169: tsk_fs_make_ls is now supported as an external library\nfunction. Now named tsk_fs_meta_make_ls.\n- 2908510: Nanosecond resolution of timestamps is now available.\n- 2914255: Version info is now available in .h files in both string\nand integer form.\n\nBug Fixes:\n- 2568528: incorrect adjustment of attribute FILLER offset.  \n- 2596397: Incorrect date sorting in mactime. \n- 2708195: Errors when doing long reads in fragmented attributes.\n- Fixed typo bugs in sorter (reported via e-mail by Drew Hunt). \n- 2734458: added orphan cache map to prevent slow NTFS listing times.\n- 2655831: Sorter now knows about the ext2 and ext3 types. \n- 2725799: ifind not converting UTF16 names properly on Windows\nbecause it was using endian ordering of file system and not local\nsystem.\n- 2662168: warning messages on macs when reading the raw character\ndevice.\n- 2778170: incorrect read size on resident attributes.\n- 2777633: missing second resolution on FAT creation times.\n- Added the READ_SHARE option to the CreateFile command for split\nimage files.  Patch by Christopher Siwy.\n- 2786963: NTFS compression infinite loop fix.\n- 2645156: FAT / blkls error getting slack because allocsize was \nbeing set too small (and other values were not being reset).  \n- 2367426: Zeros are set for VDL slack on NTFS files.  \n- 2796945: Inifite loop in fs_attr. \n- 2821031: Missing fls -m fields.\n- 2840345: Extended DOS partitions in extended partitions are now\nmarked as Meta.\n- 2848162: Reading attributes at offsets that are on boundary of\nrun fragment.\n- 2824457: Fixed issue reading last block of file system with blkcat. \n- 2891285: Fixed issue that prevented reads from the last block of\na file system when using the POSIX-style API.\n- 2825690: Fixed issue that prevented blkls -A from working.  \n- 2901365: Allow FAT files to have a 0 wdate.\n- 2900761: Added FAT directory sanity checks to prevent infinite loops.\n- 2895607: Fixed various memory leaks. \n- 2907248: Fixed image layer cache crash.\n- 2905750: all file system read() functions now return -1 when\noffset given is past end of file.\n\n\n---------------- VERSION 3.0.1 -------------- \n11/11/08: Bug Fix: Fixed crashing bug in ifind on FAT file system.\nBug: 2265927\n\n11/11/08: Bug Fix: Fixed crashing bug in istat on ExtX $OrphanFiles\ndir. Bug: 2266104\n\n11/26/08: Update: Updated fls man page.\n\n11/30/08: Update: Removed TODO file and using tracker for bugs and\nfeature requests.\n\n12/29/08: Bug Fix: Fixed incorrectly setting block status in file_walk\nfor compressed files (Bug: 2475246)\n\n12/29/08: Bug Fix: removed fs_info field from FS_META because it\nwas not being set and should have been removed in 3.0. Reported by\nRob Joyce and Judson Powers.  \n\n12/29/08: Bug Fix: orphan files and NTFS files found via parent\ndirectory have an unknown file name type (instead of being equal\nto meta type).  (Bug: 2389901). Reported by Barry Grundy.\n\n1/12/09: Bug Fix: Fixed ISO9660 bug where large directory contents\nwere not displayed.  (Bug: 2503552).  Reported by Tom Black.\n\n1/24/09: Bug Fix: Fixed bug 2534449 where extra NTFS files were\nshown if the MFT address was changed to 0 because fs_dir_add was\nchecking the address and name.  Reported by Andy Bontoft.\n\n1/29/09: Update: Fixed fix for bug 2534449.  The fix is in ifind \ninstead of fs_dir_add().  \n\n2/2/09: Update: Added RPM spec file from Morgan Weetmam.\n\n\n---------------- VERSION 3.0.0 -------------- \n0/00/00: Update: Many, many, many API changes.\n\n2/14/08: Update: Added mmcat tool.\n\n2/26/08: Update: Added flags to mmls to specify partition types. \n\n3/1/08: Update: Major update of man pages. \n\n4/14/08: Bug Fix: Fixed the calculation of \"actual\" last block.\nOff by 1 error.  Reported by steve.\n\n5/23/08: Bug Fix: Incorrect malloc return check in srch_strings.\nreported by Petri Latvala.\n\n5/29/08: Bug Fix: Fixed endian ordering bug in ISO9660 code. Reported\nby Eduardo Aguiar de Oliveira.\n\n6/17/08: Update: 'sorter' now uses the ifind method for finding\ndeleted NTFS files (like Autopsy) does instead of relying on fls.\nReported by John Lehr.\n\n6/17/08: Update: 'ifind -p' reports data on ADS.\n\n7/10/08: Update: FAT looks for a backup boot sector in FAT32 if\nmagic is 0\n\n7/21/08: Bug Fix: Changed define of strcasecmp to _stricmp instead\nof _strnicmp in Windows.  (reported by Darren Bilby).\n\n7/21/08: Bug Fix: Fall back to open \"\\\\.\\\" image files on Windows\nwith SHARE_WRITE access so that drive devices can be opened.\n(reported by Darren Bilby).\n\n8/20/08: Bug Fix: Look for Windows objects when opening files in\nCygwin, not  just Win32.  Reported by Par Osterberg Medina.\n\n8/21/08: Update: Renamed library and install header files to have a '3'\nin them to allow parallel installations of v2 and v3.  Suggested by\nSimson Garfinkel.\n\n8/22/08: Update: Added -b option to sorter to specify minimum file size\nto process.  Suggested by Jeff Kell. \n\n8/22/08: Update: Added libewf as a requirement to build win32 so that \nE01 files are supported. \n\n8/29/08: Update: Added initial mingw patches for cross compiling and\nWindows.  Patches by Michael Cohen.\n\n9/X/08: Update: Added ability to access attibutes\n\n9/6/08: Update: Added image layer cache.\n\n9/12/08: Bug Fix: Fixed crash from incorrectly cleared value in FS_DIR \nstructure.  Reported and patched by Jason Miller.\n\n9/13/08: Update: Changed d* tool names to blk*.\n\n9/17/08: Update: Finished mingw support so that both tools and\nlibrary work with Unicode file name support.  \n\n9/22/08: Update: Added new HFS+ code from Judson Powers and Rob Joyce (ATC-NY)\n\n9/24/08: Bug Fix: Fixed some cygwin compile errors about types on Cygwin.\nReported by Phil Peacock.\n\n9/25/08: Bug Fix: Added O_BINARY to open() in raw and split because Cygwin\nwas having problems. Reported by Mark Stam.\n\n10/1/08: Update: Added ifndef to TSK_USE_HFS define to allow people\nto define it on the command line.  Patch by RB.\n\n\n---------------- VERSION 2.52 --------------\n2/12/08: Bug Fix: Fixed warning messages in mactime about non-Numeric\ndata.  Reported by Pope.\n\n2/19/08: Bug Fix: Added #define to tsk_base_i.h to define\nLARGEFILE64_SOURCE based on LARGEFILE_SOURCE for older Linux systems.\n\n2/20/08: Bug Fix: Updated afflib references and code.\n\n3/13/08: Update: Added more fixes to auto* so that AFF will compile\non more systems.  I have confirmed that AFFLIB 3.1.3 will run with\nOS X 10.4.11.\n\n3/14/08: Bug Fix: Added checks to FAT code that calcs size of\ndirectories.  If starting cluster of deleted dir points into a\ncluster chain, then problems can occur.  Reported by John Ward.\n\n3/19/08: Update: I have verified that this compiles with libewf-20070512.\n\n3/21/08: Bug Fix: Deleted Ext/FFS directories were not being recursed\ninto.  This case was rare (because typically the metadata  are\nwiped), but possible.  Reported by JWalker.\n\n3/24/08: Update: I have verified that this compiles with libewf-20080322.\nUpdates from Joachim Metz.\n\n3/26/08: Update: Changed some of the header file design for the tools\nso that the define settings in tsk_config.h can be used (for large files).\n\n3/28/08: Update: Added config.h reference to srch_strings to get the \nLARGEFILE support.\n\n4/5/08: Update: Improved inode argument number parsing function. \n\n\n---------------- VERSION 2.51 --------------\n1/30/08: Bug Fix: Fixed potential infinite loop in fls_lib.c. Patch\nby Nathaniel Pierce.\n\n2/7/08: Bug Fix: Defined some of the new constants that are used\nin disktools because older Linux distros did not define them.\nReported by Russell Reynolds.\n\n2/7/08: Bug Fix: Modified autoconf to check for large file build\nrequirements and look for new 48-bit structures needed by disktools.\nBoth of these were causing problems on older Linux distros.\n\n2/7/08: Update: hfind will normalize hash values in database so \nthat they are case insensitive.\n\n---------------- VERSION 2.50 --------------\n12/19/07: Update: Finished upgrade to autotools building design. No\nlonger include file, afflib, libewf. Resulted in many source code layout\nchanges and sorter now searches for md5, sha1, etc. \n\n---------------- VERSION 2.10 --------------\n7/12/07: Update: 0s are returned for AFF pages that were not imaged.  \n\n7/31/07: Bug Fix: ifind -p could crash if a deleted file name was found\nthat did not point to a valid meta data stucture.  (Reported by Andy Bontoft)\n\n8/5/07: Update: Added NSRL support back into sorter.\n\n8/15/07: Update: Errors are given if supplied sector offset is larger than \ndisk image.  Reported by Simson Garfinkel.\n\n8/16/07: Update: Renamed MD5 and SHA1 functions to TSK_MD5_.. and TSK_SHA_....\n\n8/16/07: Update: tsk_error_get() does not reset the error messages.\n\n9/26/07: Bug Fix: Changed FATFS check for valid dentries to consider\nsecond values of 30.  Reported by Alessandro Camillo.\n\n10/18/07: Update: inode_walk for NTFS and FAT will not abort if\ndata corruption is found in one entry -- instead they will just\nskip it.\n\n10/18/07: Update: tsk_os.h uses standard gcc system names instead\nof TSK specific ones.\n\n10/18/07: Update: Updated raw.c to use ioctl commands on OS X to\nget size of raw device because it does not work with SEEK_END.\nPatch by Rob Joyce.\n\n10/31/07: Update: Finished upgrade to fatfs_file_walk_off so that\nwalking can start at a specific offset.  Also finished upgrade that\ncaches FAT run list to make the fatfs_file_walk_off more efficient.\n\n11/14/07: Update: Fixed few places where off_t was being used \ninstead of OFF_T.  Reported by GiHan Kim. \n\n11/14/07: Update: Fixed a memory leak in aff.c to free AFF_INFO.\nReported by GiHan Kim.\n\n11/24/07: Update: Finished review and update of ISO9660 code. \n\n11/26/07: Bug Fix: Fixed 64-bit calculation in HFS+ code.  Submitted\nby Rob Joyce. \n\n11/29/07: Update: removed linking of srch_strings.c and libtsk.  Reported by\nkwizart.\n\n11/30/07: Upate: Made a #define TSK_USE_HFS compile flag for incorporating\nthe HFS support (flag is in src/fstools/fs_tools_i.h)\n\n11/30/07: Update: restricted the FAT dentry sanity checks to verify\nspace padding in the name and latin-only extensions. \n\n12/5/07: Bug Fix: fs_read_file_int had a bug that ignored the type passed\nfor NTFS files.  Reported by Dave Collett.\n\n12/12/07: Update: Changed teh FAT dentry sanity checks to allow spaces\nin volume labels and do more checking on the attribute flag.\n\n\n\n\n---------------- VERSION 2.09 --------------\n4/6/07: Bug Fix: Inifite loop in ext2 and ffs istat code because of using\nunsigned size_t variable.  Reported by Makoto Shiotsuki.\n\n4/16/07: Bug Fix: Changed use of fseek() to fseeko() in hashtools.  Patch \nby Andy Bontoft.\n\n4/16/07: Bug Fix: Changed Win32 SetFilePointer to use LARGE_INTEGER.\nReported by Kim GiHan.\n\n4/19/07: Bug Fix: Not all FAT orphan files were being found because of\nand offset error.\n\n4/26/07: Bug Fix: ils -O was not working (link value not being\nchecked).  Reported by Christian Perst.\n\n4/27/07: Bug Fix: ils -r was showing UNUSED inodes.  Reported by\nChristian Perst.\n\n5/10/07: Update: Redefined the USED and UNUSED flags for NTFS so that \nUNUSED is set when no attributes exist. \n\n5/16/07: Bug Fix: Fixed several bounds checking bugs that may cause\na crash if the disk image is corrupt.  Reported by Tim Newsham (iSec \nPartners)\n\n5/17/07: Update: Updated AFFLIB to 2.2.11\n\n5/17/07: Update: Updated libewf to libewf-20070512\n\n5/17/07: Update: Updated file to 4.20\n\n5/29/07: Update: Removed NTFS SID/SDS contributed code because it causes \ncrashes on some systems and its output is not entirely clear. (most recent bug\nreported by Andy Scott)\n\n6/11/07: Update: Updated AFFLIB to 2.2.12.\n\n6/12/07: Bug Fix: ifind -p was not reporting back info on the allocated name\nwhen one existed (because strtok was overwritting the name when the search\ncontinued).   Reported by Andy Bontoft. \n\n6/13/07: Update: Updated file to 4.21\n\n\n---------------- VERSION 2.08 --------------\n12/19/06: Bug Fix: ifind_path was not setting *result when root inode\nwas searched for.  patch by David Collett.\n\n12/29/06: Update: Removed 'strncpy' in ntfs.c to manual assignment of \ntext for '$Data' and 'N/A' for performance reasons. \n\n1/11/07: Update: Added duname to FS_INFO that contains a string of \nname for a file system's data unit -- Cluster for example.\n\n1/19/07: Bug Fix: ifind_path was returning an error even after some\nfiles were found.  Errors are now ignored if a file was found.  \nReported by Michael Cohen. \n\n1/26/07: Bug Fix: Fixed calcuation of inode numbers in fatfs.c \n(reported by Simson Garfinkel).\n\n2/1/07: Update: Changed aff-install to support symlinked directory. \n\n2/1/07: Update: img_open modified so that it does not report errors for\ns3:// and http:// files that do not exist.\n\n2/5/07: Update: updated *_read() return values to look for \"<0\" instead of \nsimply \"== -1\". (suggested by Simson Garfinkel).\n\n2/8/07: Update: removed typedef for uintptr in WIN32 code. \n\n2/13/07: Update: Applied patch from Kim Kulak to update HFS+ code to internal \ndesign changes. \n\n2/16/07: Update: Renamed many of the external data structures and flags\nso that they start with TSK_ or tsk_ to prevent name collisions.\n\n2/16/07: Update: Moved MD5 and SHA1 routines and binaries to auxtools\ninstead of hashtools so that they are more easy to access.\n\n2/16/07: Update: started redesign and port of hashtools.\n\n2/21/07: Update: Changed inode_walk callback API to remove the flags\nvariable -- this was redundant since flags are also in TSK_FS_INODE.\nSame for TSK_FS_DENT.\n\n3/7/07: Bug Fix: fs_read_file failed for NTFS resident files.  Reported\nby Michael Cohen.\n\n3/8/07: Bug Fix: FATFS assumed a 512-byte sector in a couple of locations.\n\n3/13/07: Update: Finished hashtools update.\n\n3/13/07: Update: dcat reads block by block instead of all at once.\n\n3/23/07: Update: Change ntfs_load_secure to allocate all of its\nneeded memory at once instead of doing reallocs.\n\n3/23/07: Update: Updated AFFLIB to 2.2.0\n\n3/24/07: Bug Fix: Fixed many locations where return value from strtoull\nwas not being properly checked and therefore invalid numbers were not\nbeing detected.\n\n3/24/07: Bug Fix: A couple of error messages in ntfs_file_walk should\nhave been converted to _RECOVER when the _RECOVERY flag was given. \n\n3/24/07: Update: Changed behavior of ntfs_file_walk.  If no type is\ngiven, then a default type is chosen for files and dirs.  Now, no error\nis generated if that type does not exist -- similar to how no error is\ngenerated if a FAT file has 0 file size.\n\n3/26/07: Update: cleaned up and documented fs_data code more.\n\n3/29/07: Update: Updated AFF to 2.2.2.\n\n3/29/07: Update: Updated install scripts for afflib, libewf, and file to\ntouch files so that the auto* files are in the correct time stamp order.\n\n4/5/07: Bug Fix: Added sanity checks to offsets and addresses in ExtX and\nUFS group descriptors.  Reported by Simson Garfinkel. \n\n\n---------------- VERSION 2.07 --------------\n9/6/06: Update: Changed TCHAR and _T to TSK_TCHAR and _TSK_T to avoid\nconflicts with other libraries.\n\n9/18/06: Update: Added tsk_list_* functions and structures.\n\n9/18/06: Update: Added checks for recursive FAT directories. \n\n9/20/06: Update: Changed FS_META_* flags for LINK and UNLINK and moved\nthem to ILS_? flags.\n\n9/20/06: Update: added flags to ils to find only orphan inodes.\n\n9/20/06: Update: Added Orphan support for FAT, NTFS, UFS, Ext2, ISO. \n\n9/20/06: Update: File walk actions now have a flag to identify if a block\nis SPARSE or not (used to identify if the address being passed is valid\nor made up).\n\n9/21/06: Update: Added file size sanity check to fatfs_is_dentry and\nfixed assignment of fatfs->clustcnt.\n\n9/21/06: Update: block_, inode, and dent_walk functions now do more flag\nchecking and make sure that some things are set instead of making the\ncalling code do it.\n\n9/21/06: Update: Added checks for recursive (infinite loop) NTFS, UFS,\nExtX, and ISO9660 directories.\n\n9/21/06: Update Added checks to make sure that walking the FAT for files\nand directories would result in an infinite loop (if FAT is corrupt).\n\n9/21/06: Update: Added -a and -A to dls to specify allocated and\nunallocated blocks to display.\n\n9/21/06: Update: Updated AFFLIB to 1.6.31. \n\n9/22/06: Update: added a fs_read_file() function that allows you to read\nrandom parts of a file. \n\n10/10/06: Update: Improved performance of fs_read_file() and added\nnew FS_FLAG_META_COMP and FS_FLAG_DATA_COMP flags to show if a file\nand data are using file system-level compression (NTFS only).\n\n10/18/06: Bug fix: in fs_data_put_run, added a check to see\nif the head was null before looking up.  An extra error message\nwas being created for nothing. \n\n10/18/06: Bug Fix: Added a check to the compression buffer \nto see if it is null in _done(). \n\n10/25/06: Bug Fix: Added some more bounds checks to NTFS uncompression code.\n\n11/3/06: Bug Fix: added check to dcat_lib in case the number of blocks\nrequested is too large. \n\n11/07/06: Update: Added fs_read_file_noid wrapper around fs_read_file\ninterface.\n\n11/09/06: Update: Updated AFF to 1.7.1\n\n11/17/06: Update: Updated libewf to 20061008-1\n\n11/17/06: Bug Fix: Fixed attribute lookup bug in fs_data_lookup.\nPatch by David Collett.\n\n11/21/06: Bug Fix: Fixed fs_data loops that were stopping when they hit\nan unused attribute.  Patch by David Collett.\n\n11/21/06: Bug Fix: sorter no longer clears the path when it starts. THis\nwas causing errors on Cygwin because OpenSSL libraries could not be found.\n\n11/22/06: Update: Added a tskGetVersion() function to return the string\nof the current version. \n\n11/29/06: Update: Added more tsk_error_resets to more places to prevent\nextra error messages from being displayed. \n\n11/30/06: Update: Added Caching to the getFAT function and to fs_read.\n\n12/1/06: Update: Changed TSK_LIST to a reverse sorted list of buckets. \n\n12/5/06: Bug Fix: Fixed FS_DATA_INUSE infinite loop bug.\n\n12/5/06: Bug Fix: Fixed infinite loop bug with NTFS decompression code.\n\n12/5/06: Update: Added NULL check to fs_inode_free (from Michael Cohen).\n\n12/5/06: Update: Updated ifind_path so that an allocated name will be\nshown if one exists -- do not exit if we find simply an unallocated\nentry with an address of 0. Suggested by David Collett.\n\n12/6/06: Update: Updated file to version 4.18.\n\n12/6/06: Update: Updated libaff to 2.0a10 and changed build process\naccordingly.\n\n12/7/06: Update: Added a tsk_error_get() function that returns a string\nwith the error messages -- can be used instead of tsk_error_print.\n\n12/7/06: Update: fixed some memory leaks in FAT and NTFS code. \n\n12/11/06: Bug Fix: fatfs_open error message code referenced a value that\nwas in freed memory -- reordered statements.  \n\n12/15/06: Update: Include VCProj files in build.\n\n\n---------------- VERSION 2.06 --------------\n8/11/06: Bug Fix: Added back in ASCII/UTF-8 checks to remove control\ncharacters in file names. \n\n8/11/06: Bug Fix: Added support for fast sym links in UFS1\n\n8/11/06: Update: Redesigned the endian support so that getuX takes only\nthe endian flag so that the Unicode design could be changed as well.\n\n8/11/06: Update: Redesigned the Unicode support so that there is a\ntsk_UTF... routine instead of fs_UTF...\n\n8/11/06: Update: Updated GPT to fully convert UTF16 to UTF8.\n\n8/11/06: Update: There is now only one aux_tools header file to include\ninstead of libauxtools and/or aux_lib, which were nearly identical. \n\n8/16/06: Bug Fix: ntfs_dent_walk could segfault if two consecutive\nunallocated entries were found that had an MFT entry address of 0.\nReported by Robert-Jan Mora.\n\n8/16/06: Update: Changed a lot of the header files and reduced them so\nthat it is easier to use the library and only one header file needs to\nbe included.\n\n8/21/06: Update: mmtools had char * instead of void * for walk callback\n\n8/22/06: Update: Added fs_load_file function that returns a buffer full \nwith the contents of a file.\n\n8/23/06: Update: Upgraded AFFLIB to 1.6.31 and libewf to 20060820-1.\n\n8/25/06: Update: Created printf wrappers so that output is UTF-16 on\nWindows and UTF-8 on Unix. \n\n8/25/06: Update: Continued port to Windows by starting to use more\nTCHARS and defining needed macros for the Unix side. \n\n8/25/06: Bug Fix: Fixed crash that could occur because of SDS code\nin NTFS.  (reported by Simson Garfinkel) (BUG: 1546925).\n\n8/25/06: Bug Fix: Fixed crash that could occur because path stack became\ncorrupt with deep directories or corrupt images. (reported by Simson \nGarfinkel) (BUG: 1546926).\n\n8/25/06: Bug Fix: Fixed infinite loop that could occur when trying to\ndetermine size of FAT directory when the FAT has a loop in it. (BUG:\n1546929)\n\n8/25/06: Update: Improved FAT checking code to look for '.' and '..'\nentries when inode value is replaced during dent_walk.\n\n8/29/06: Update: Finished Win32 port and changes to handle UTF-16 vs\nUTF-8 inputs.  \n\n8/29/06: Update: Created a parse_inum function to handle parsing inode\naddresses from command line. \n\n8/30/06: Update: Made progname a local variable instead of global. \n\n8/31/06: Bug Fix: Fixed a sizeof() error with the memset in fatfs_inode_walk\nfor the sect_alloc buffer. \n\n8/31/06: Update: if mktime in dos2unixtime returns any negative value,\nthen the return value is set to 0.  Windows and glibc seem to have\ndifferent return values.\n\n---------------- VERSION 2.05 --------------\n5/15/06: Bug Fix: Fixed a bug in img_cat that could cause it to\ngo into an infinite loop.  (BUG: 1489284)\n\n5/16/06: Update: Fixed printf statements in tsk_error.c that caused\nwarning messages for some compilers.  Reported by Jason DePriest.\n\n5/17/06: Update: created a union of file system-specific file times in\nFS_INFO (Patch by Wyatt Banks)\n\n5/22/06: Bug Fix: Updated libewf to 20060520 to fix bug with reported\nimage size. (BUG: 1489287)\n\n5/22/06: Bug Fix: Updated AFFLIB to 1.6.24 so that TSK could compile in \nCYGWIN. (BUG: 1493013)\n\n5/22/06: Update: Fixed some more printf statements that were causing\ncompile warnings. \n\n5/23/06: Update: Added a file existence check to img_open to make error \nmessage more accurate.\n\n5/23/06: Update: Usage messages had extra \"Supported image types message\".\n\n5/25/06: Update: Added block / page range to fsstat for raw and swapfs.\n\n6/5/06: Update: fixed some typos in the output messages of sigfind (reported\nby Jelle Smet)\n\n6/9/06: Update: Added HFS+ template to sigfind (Patch by Wyatt Banks)\n\n6/9/06: Update: Added ntfs and HFS template to sigfind.\n\n6/19/06: Update: Begin Windows Visual Studio port\n\n6/22/06: Update: Updated a myflags check in ntfs.c (reported by Wyatt Banks)\n\n6/28/06: Update: Incorporated NTFS compression patch from I.D.E.A.L.\n\n6/28/06: Update: Incorporated NTFS SID patch from I.D.E.A.L.\n\n6/28/06: Bug Fix: A segfault could occur with NTFS if no inode was loaded\nin the dent_walk code.  (Reported by Pope).\n\n7/5/06: Update: Added tsk_error_reset function and updated code to use it.\n\n7/5/06: Update: Added more sanity checks to the DOS partitions code.\n\n7/10/06: Update: Upgraded libewf to version 20060708.\n\n7/10/06: Update: Upgraded AFFLIB to version 1.6.28\n\n7/10/06: Update: added 'list' option to usage message so that file\nsystem, image, volume system types are listed only if '-x list' is given.\nSuggested by kenshin.\n\n7/10/06: Update: Compressed NTFS files use the compression unit size\nspecified in the header.\n\n7/10/06: Update: Added -R flag to icat to suppress recovery warnings and\nuse this flag in sorter to prevent FAT recovery messages from filling\nup screen.  \n\n7/10/06: Update: file_walk functions now return FS_ERR_RECOVERY error\ncodes for most cases if the RECOVERY flag is set -- this allows the\nerrors to be more easily suppressed.\n\n7/12/06: Update: Removed individual libraries and now make a single\nstatic libtsk.a library.\n\n7/12/06: Update: Cleaned up top-level Makefile.  Use '-C' flag (suggested\nby kenshin).\n\n7/14/06: Update: Fixed and redesigned some of the new NTFS compression\ncode.  Changed variable names.\n\n7/20/06: Update: Fixed an NTFS compression bug if a sub-block was not\ncompressed.\n\n7/21/06: Update: Made NTFS compression code thread friendly.\n\n\n---------------- VERSION 2.04 --------------\n12/1/05: Bug Fix: Fixed a bug in the verbose output of img_open\nthat would crash if no type or offset was given.  Reported and\npatched by Wyatt Banks.\n\n12/20/05: Bug Fix: An NTFS directory index sanity check used 356\ninstead of 365 when calculating an upper bound on the times.  Reported\nby Wyatt Banks.\n\n12/23/05: Bug Fix: Two printf statements in istat for NTFS printed\nto stdout instead of a specific file handle. Reported by Wyatt\nBanks.\n\n1/22/06: Bug Fix: fsstat, imgstat and dcalc were using a char instead\nof int for the return value of getopt, which caused some systems to not\nexecute the programs. (internal fix and later reported by Bernhard Reiter)\n\n2/23/06: Update: added support for FreeBSD 6.\n\n2/27/06: Bug Fix: Indirect blocks would nto be found by ifind with\nUFS and Ext2.  Reported by Nelson G. Mejias-Diaz.  (BUG: 1440075)\n\n3/9/06: Update: Added AFF image file support.\n\n3/14/06: Bug Fix: If the first directory entry of a UFS or ExtX block\nwas unallocated, then later entries may not be shown. Reported by John\nLangezaal.  (BUG: 1449655)\n\n4/3/06: Update: Finished the improved error handling.  Many internal\nchanges, not many external changes.  error() function no longer used\nand instead tsk_err variables and function are used.  This makes the\nlibrary more powerful.\n\n4/5/06: Update: The byte offset for a volume is now passed to the mm_\nand fs_ functions instead of img_open.  This allows img_info to be used\nfor multiple volumes at the same time. This required some mm_ changes.\n\n4/5/06: Update: All TSK libraries are written to the lib directory.\n\n4/6/06: Update: Added FS_FLAG_DATA_RES flag to identify data that are\nresident in ntfs_data_walk (suggested by Michael Cohen).\n\n4/6/06: Update: The partition code (media Management) now checks that a\npartition starts before the end of the image file.  There are currently\nno checks about the end of the partition though.\n\n4/6/06: Update: The media management code now shows unpartitioned space\nas such from the end of the last partition to the end of the image file\n(using the image file size).  (Suggested by Wyatt Banks).\n\n4/7/06: Update: New version of ISO9660 code from Wyatt Banks and Crucial\nSecurity added and other code updated to allow CDs to be analyzed.\n\n4/7/06: There was a conflict with guessuXX with mmtools and fstools.\nRenamed to mm_guessXX and fs_guessXX.\n\n4/10/06: Upgraded AFFLIB to 1.5.6\n\n4/12/06: Added version of libewf and support for it in imgtools\n\n4/13/06: Added new img_cat tool to extract raw data from an image format.\n\n4/24/06: Upgraded AFFLIB to 1.5.12\n\n4/24/06: split and raw check if the image is a directory \n\n4/24/06: Updated libewf to 20060423-1\n\n4/26/06: Updated makedefs to work with SunOS 5.10\n\n5/3/06: Added iso9660 patch from Wyatt Banks so that version number\nis not printed with file name.\n\n5/4/06: Updated error checking in icat, istat, fatfs_dent, and ntfs_dent\n\n5/8/06: Updated libewf to 20060505-1 to fix some gcc 2 compile errors.\n\n5/9/06: Updated AFFLIB to 1.6.18\n\n5/11/06: Cleaned up error handling (removed %m and unused legacy code)\n\n5/11/06: Updated AFFLIB to 1.6.23\n\n---------------- VERSION 2.03 --------------\n7/26/05: Update: Removed incorrect print_version() statement from\nfs_tools.h (reported by Jaime Chang)\n\n7/26/05: Update: Renamed libraries to start with \"lib\"\n\n7/26/05: Update: Removed the logfp variable for verbose statements\nand instead use only stderr.\n\n8/12/05: Update: If time is 0, then it is put as 00:00:00 instead of\nthe default 1970 or 1980 time. \n\n8/13/05: Update: Added Unicode support for FAT and NTFS (Supported by\nI.D.E.A.L. Technology Corp).\n\n9/2/05: Update: Added Unicode support for UFS and ExtX.  Non-printable\nASCII characters are no longer replaced with '^.'.  \n\n9/2/05: Update: Improved the directory entry sanity checks for UFS\nand ExtX.\n\n9/2/05: Update: Upgraded file to version 4.15.\n\n9/2/05: Update: The dent_walk code of all file systems does not\nabort if a sub-directory is encountered with an error.  If it is the\ntop directory explicitly called, then it still gives an error.\n\n9/2/05: Bug Fix: MD5 and SHA-1 values were incorrect under AMD64 \nsystems because the incorrect variable sizes were being used.\n(reported by: Regis Friend Cassidy. BUG: 1280966)\n\n9/2/05: Update: Changed all licenses in TSK to Common Public License\n(except those that were already IBM Public License).\n\n9/15/05: Bug Fix: The Unicode names would not be displayed if the FAT\nshort name entry was using code pages.  The ASCII name check was removed,\nwhich may lead to more false positives during inode_walk.\n\n10/05/05: Update: improved the sector size check when the FAT boot\nsector is read (check for specific values besides just mod 512).\n\n10/12/05: Update: The ASCII name check was added back into FAT, but\nthe check no longer looks for values over 0x80.\n\n10/12/05: Update: The inode_walk function in FAT skips clusters\nthat are allocated to files.  This makes it much faster, but it\nwill now not find unallocated directory entries in the slack space\nof allocated files.\n\n10/13/05: Update: sorter updated to handle unicode in HTML output.\n\n---------------- VERSION 2.02 --------------\n4/27/05: Bug Fix: the sizes of 'id' were not consistent in the\nfront-end and library functions for icat and ffind.  Reported by\nJohn Ward.\n\n5/16/05: Bug Fix: fls could segfault in FAT if short name did not\nexist.  There was also a bug where the long file name variable\n(fatfs->lfn_len) was not reset after processing a directory and the\nnext entry could incorrectly get the long name.  Reported by Jaime\nChang.  BUG: 1203673.\n\n5/18/05: Update: Updated makedefs to support Darwin 8 (OS X Tiger)\n\n5/23/05: Bug Fix: ntfs_dent_walk would not always stop when WALK_STOP\nwas returned.  This caused some issues with previous versions of ifind.\nThis was fixed.\n\n5/24/05: Bug Fix: Would not compile under Suse because it had header\nfile conflicts for the size of int64_t. Reported by: Andrea Ghirardini.\nBUG: 1203676\n\n5/25/05: Update: Fixed some memory leaks in fstools (reported by Jaime\nChang).\n\n6/13/05: Update: Compiled with g++ to get better warning messages.\nFixed many signed versus unsigned comparisons, -1 assignments to\nunsigned vars, and some other minor internal issues.\n\n6/13/05: Bug Fix: if UFS or FFS found a valid dentry in unallocated\nspace, it could have a documented length that is larger than the\nremaining unallocated space.  This would cause an allocated name\nto be skipped.  BUG: 1210204  Reported by Christopher Betz.\n\n6/13/05: Update: Improved design of all dent code so that there are no \nmore global variables.  \n\n6/13/05: Update: Improved design of FAT dent code so that FATFS_INFO\ndoes not keep track of long file name information.\n\n6/13/05: Bug Fix: If a cluster in a directory started with a strange\ndentry, then FAT inode_walk would skip it.  The fixis to make sure\nthat all directory sectors are processed.  (BUG: 1203669).  Reported\nby Jaime Chang.\n\n6/14/05: Update: Changed design of FS_INODE so that it contains the\ninode address and the inode_walk action was changed to remove inum\nas an argument.\n\n6/15/05: Update: Added 'ils -o' back in as 'ils -O' to list open\nand deleted files.\n\n6/15/05: Update: Added '-m' flag to mactime so that it prints the month\nas a number instead of its name.\n\n7/2/05: Bug Fix: If an NTFS file did not have a $DATA or $IDX_*\nattribute, then fls would not print it.  The file had no content, but\nthe name should be shown.  (BUG: 1231515) (Reported by Fuerst)\n\n\n---------------- VERSION 2.01 --------------\n3/24/05: Bug Fix: ffind would fail if the directory had two\nnon-printable chars.  The handling of non-printable chars was changed\nto replace with '^.'.  (BUG: 1170310) (reported by Brian Baskin)\n\n3/24/05: Bug Fix: icat would not print the output to stdout when split\nimages were used.  There was a bug in the image closing process of\nicat.  (BUG: 1170309) (reported by Brian Baskin)\n\n3/24/05: Update: Changed the header files in fstools to make fs_lib.h\nmore self contained.\n\n4/1/05: Bug Fix: Imgtools byte offset with many leading 0s could\ncause issues.  (BUG: 1174977)\n\n4/1/05: Update: Removed test check in mmtools/dos.c for value cluster\nsize because to many partition tables have that as a valid field.\nNow it checks only OEM name.\n\n4/8/05: Update: Updated usage of 'strtoul' to 'strtoull' for blocks\nand inodes.\n\n---------------- VERSION 2.00 --------------\n1/6/05: Update: Added '-b' flag to 'mmls' so that sizes can be\nprinted in bytes.  Suggested and a patch proposed by Matt Kucenski\n\n1/6/05: Update: Define DADDR_T, INUM_T, OFF_T, PNUM_T as a static\nsize and use those to store values in data structures.   Updated\nprint statements as well.\n\n1/6/05: Update: FAT now supports larger images becuase the inode\naddress space is 64-bits.\n\n1/6/05: Moved guess and get functions to misc from mmtools and\nfstools.\n\n1/7/05: Update: Added imgtools with support for \"raw\" and \"split\"\nlayers.  All fstools have been updated.\n\n1/7/05: Update: removed dtime from ils output\n\n1/9/05: Update: FAT code reads in clusters instead of sectors to\nbe faster (suggested by David Collett)\n\n1/9/05: Update: mmtools uses imgtools for split images etc.\n\n1/10/05: Update: Removed usage of global variables when using\nfile_walk internally.\n\n1/10/05: Update: mmls BSD will use the next sector automatically\nif the wrong is given instead of giving an error.\n\n1/10/05: Update: Updated file to version 4.12\n\n1/11/05: Update: Added autodetect to file system tools.\n\n1/11/05: Update: Changed names to specify file system type (not\nOS-based)\n\n1/11/05: Update: Added '-t' option to fsstat to give just the type.\n\n1/11/05: Update: Added autodetect to mmls\n\n1/17/05: Update: Added the 'mmstat' tool that gives the type of\nvolume system.\n\n1/17/05: Update: Now using CVS for local version control - added\ndate stamps to all files.\n\n2/20/05: Bug Fix: ils / istat would go into an infinte loop if the\nattribute list had an entry with a length of 0.  Reported by Angus\nMarshall (BUG: 1144846)\n\n3/2/05: Update: non-printable letters in ExtX/UFS file names are\nnow replaced by a '.'\n\n3/2/05: Update: Made file system tools more library friendly by \nmaking stubs for each application.\n\n3/4/05: Update: Redesigned the diskstat tool and created the\ndisksreset tool to remove the HPA temporarily.\n\n3/4/05: Update: Added imgstat tool that displays image format\ndetails\n\n3/7/05: Bug Fix: In fsstat on ExtX, the final group would have an\nincorrect _percentage_ of free blocks value (although the actual\nnumber was correct).  Reported by Knut Eckstein.  (BUG: 1158620)\n\n3/11/05: Update: Renamed diskstat, disksreset, sstrings, and imgstat to\ndisk_stat, disk_sreset, srch_strings, and img_stat to make the names more\nclear. \n\n3/13/05: Bug Fix: The verbose output for fatfs_file_walk had an\nincorrect sector address.  Reported by Rudolph Pereira.\n\n3/13/05: Bug Fix: The beta version had compiling problems on FreeBSD\nbecause of a naming clash with the new 'fls' functions. (reported\nby secman)\n\n\n\n---------------- VERSION 1.74 --------------\n11/18/04: Bug Fix: FreeBSD 5 would produce incorrect 'icat' output for\nExt2/3 & UFS1 images because it used a 64-bit on-disk address. \nreported by neutrino neutrino.  (BUG: 1068771)\n\n11/30/04: Bug Fix: The makefile in disktools would generate an error\non some systems (Cygwin) because of an extra entry.  Reported by\nVajira Ganepola (BUG: 1076029)\n\n\n---------------- VERSION 1.73 --------------\n09/09/04: Update: Added journal support for EXT3FS and added jls\nand jcat tools.\n\n09/13/04: Updated: Added the major and minor device numbers to\nEXTxFS istat.\n\n09/13/04: Update: Added EXTxFS orphan code to 'fsstat'\n\n09/24/04: Update: Fixed incorrect usage of 'ptr' and \"\" in action\n  of ntfs_dent.c.  Did not affect any code, but could have in the\n  future.  Reported by Pete Winkler.\n\n09/25/04: Update: Added UFS flags to fsstat\n\n09/26/04: Update: All fragments are printed for indirect block pointer\n  addresses in UFS istat.\n\n09/29/04: Update: Print extended UFS2 attributes in 'istat'\n\n10/07/04: Bug Fix: Changed usage of (int) to (uintptr_t) for pointer\narithmetic. Caused issues with Debian Sarge. (BUG: 1049352) - turned out\nto be from changes made to package version so that it would compile in\n64-bit system (BUG: 928278).\n\n10/11/04: Update: Added diskstat to check for HPA on linux systems.\n\n10/13/04: Update: Added root directory location to FAT32 fsstat output\n\n10/17/04: Bug Fix: EXTxFS superblock location would not be printed\nfor images in fsstat that did not have sparse superblok (which is\nrare)  (BUG: 1049355)\n\n10/17/04: Update: Added sigfind tool to find binary signatures.\n\n10/27/04: Bug Fix: NTFS is_clust_alloc returned an error when loading\n  $MFT that had attribute list entry.  Now I assume that clusters \n  referred to by the $MFT are allocated until the $MFT is loaded.\n  (BUG: 1055862).\n\n10/28/04: Bug Fix: Check to see if an attribute with the same name\n  exists instead of relying on id only. (ntfs_proc_attrseq) Affects\n  the processing of attribute lists.  Reported by Szakacsits Szabolcs,\n  Matt Kucenski, & Gene Meltser (BUG: 1055862)\n\n10/28/04: Update: Removed usage of mylseek in fstools for all systems\n  (Bug: 928278)\n\n\n---------------- VERSION 1.72 --------------\n07/31/04: Update: Added flag to mft_lookup so that ifind can run in noabort\nmode and it will not stop when it finds an invalid magic value.\n\n08/01/04: Update: Removed previous change and removed MAGIC check\nentirely.  XP doesn't even care if the Magic is corrupt, so neither\ndoes TSK.  The update sequence check should find an invalid MFT\nentry.\n\n08/01/04: Update: Added error message to 'ifind' if none of the search\noptions are given.\n\n08/05/04: Bug Fix: Fixed g_curdirptr recursive error by clearing the value\nwhen dent_walk had to abort because a deleted directory could not be recovered.\n(BUG:  1004329)  Reported by epsilon@yahoo.com\n\n08/16/04: Update: Added a sanity check to fatfs.c fat2unixtime to check\nif the year is > 137 (which is the overflow date for the 32-bit UNIX time).\n\n08/16/04: Update: Added first version of sstrings from binutils-2.15\n\n08/20/04: Bug Fix: Fixed a bug where the group number for block 0 of an \nEXT2FS file system would report -1. 'dstat' no longer displays value when it\nis not part of a block group. (BUG: 1013227)\n\n8/24/04: Update: If an attribute list entry is found with an invalid MFT\nentry address, then it is ignored instead of an error being generated and\nexiting.\n\n8/26/04: Update: Changed internal design of NTFS to make is_clust_alloc\n\n8/26/04: Update: If an attribute list entry is found with an invalid MFT\nentry address AND the entry is unallocated, then no error message is \nprinted, it is just ignored or logged in verbose mode.\n\n8/29/04: Update: Added support for 32-bit GID and UID in EXTxFS\n\n8/30/04: Bug Fix: ntfs_dent_walk was adding 24 extra bytes to the\nsize of the index record for the final record processing (calc of\nlist_len) (BUG: 1019321) (reported and debugging help from Matt\nKucenski).\n\n8/30/04: Bug Fix: fs_data_lookup was using an id of 0 as a wild\ncard, but 0 is a legit id value and this could cause confusion.  To\nsolve this, a new FS_FLAG_FILE_NOID flag was added and a new\nfs_data_lookup_noid function that will not use the id to lookup\nvalues.  (BUG: 1019690) (reported and debugging help from Matt\nKucenski)\n\n8/30/04: Update: modified fs_data_lookup_noid to return unamed data\nattribute if that type is requested (instead of just relying on id\nvalue in attributes)\n\n8/31/04: Update: Updated file to v4.10, which seems to fix the\nCYGWIN compile problem.\n\n9/1/04: Update: Added more DOS partition types to mmls (submitted by\nMatt Kucenski)\n\n9/2/04: Update: Added EXT3FS extended attributes and Posix ACL to istat\noutput.\n\n9/2/04: Update: Added free inode and block counts per group to fsstat for\nEXT2FS.\n\n9/7/04: Bug Fix: FreeBSD compile error for PRIx printf stuff in mmtools/gpt.c\n\n\n---------------- VERSION 1.71 --------------\n06/05/04: Update: Added sanity checks in fat to unix time conversion so that\ninvalid times are set to 0.\n\n06/08/04: Bug Fix: Added a type cast when size is assigned in FAT \nand removed the assignment to a 32-bit signed variable (which was no\nlonger needed).  (Bug: 966839)\n\n06/09/04: Bug Fix: Added a type cast to the 'getuX' macros because some\ncompilers were assuming it was signed (Bug: 966839).  \n\n06/11/04: Update: Changed NTFS magic check to use the aa55 at the\nend and fixed the name of the original \"magic\" value to oemname.\nThe oemname is now printed in fsstat.\n\n06/12/04: Bug Fix: The NTFS serial number was being printed with\nbytes in the wrong order in the fsstat output. (BUG: 972207)\n\n06/12/04: Update: The begin offset value in index header for NTFS \nwas 16-bits instead of 32-bits.\n\n06/22/04: Update: Created a library for the MD5 and SHA1 functions so\nthat it can be incorporated into other tools.  Also renamed some of the\nindexing tools that hfind uses.\n\n06/23/04: Update: Changed output of 'istat' for NTFS images.  Added more\ndata from $STANDARD_INFORMATION.  \n\n07/13/04: Update: Changed output of 'istat' for NTFS images again.  Moved\nmore data to the $FILE_NAME section and added new data.\n\n07/13/04: Update: Changed code for processing NTFS runs and no\nlonger check for the offset to be 0 in ntfs_make_data_run().  This\ncould have prevented some sparse files from being processed.\n\n07/13/04: Update: Added flags for compressed and encrypted NTFS\nfiles.  They are not decrypted or uncompressed yet, just identified.\nThey cannot be displayed from 'icat', but the known layout is given\nin 'istat'.\n\n07/18/04: Bug Fix: Sometimes, 'icat' would report an error about an\nexisting FILLER entry in an NTFS attribute.  This was traced to\ninstances when it was run on a non-base file record.  There is now\na check for that to not show the error. (BUG: 993459)\n\n07/19/04: Bug Fix: A run of -1 may exist for sparse files in non-NT\nversions of NTFS.  Changed check for this.  reported by Matthew\nKucenski.  (BUG: 994024).\n\n07/24/04: Bug Fix: NTFS attribute names were missing (rarely) on\nsome files because the code assumed they would always be at offset\n64 for non-res attributes (Bug: 996981).\n\n07/24/04: Update: Made listing of unallcoated NTFS file names less\nstrict.  There was a check for file name length versus stream length.\n\n07/24/04: Update: Added $OBJECT_ID output to 'istat'\n\n07/24/04: Update: Fixed ntfs.c compile warning about constant too\nlarge in time conversion code.\n\n07/25/04: Update: Added attribute list contents to NTFS 'istat' output\n\n07/25/04: Bug Fix: Not all slack space was being shown with 'dls -s'.\nIt was documented that this occurs, but it is not what would be\nexpected.  (BUG: 997800).\n\n07/25/04: Update: Changed output format of 'dls -s' so that it sends\nzeros where the file content was.  Therefore the output is now a\nmultiple of the data unit size.  Also removed limitation to FAT &\nNTFS.\n\n07/25/04: Update: 'dcalc' now has the '-s' option calculate the \noriginal location of data from a slack space image (dls -s).  \n(from Chris Betz).  \n\n07/26/04: Update: Created the fs_os.h file and adjusted some of the \nheader files for the PRI macros (C99).  Created defines for OSes that do\nnot have the macros already defined.  \n\n07/26/04: Non-release bug fix: Fixed file record size bug introduced with\nrecent changes.\n\n07/27/04: Update: Added GPT support to mmls.\n\n07/29/04: Update: Added '-p' flag to 'ifind' to find deleted NTFS files \nthat point to the given parent directory.  Added '-l and -z' as well.\n\n\n---------------- VERSION 1.70 --------------\n04/21/04: Update: Changed attribute and mode for FAT 'istat' so\nthat actual FAT attributes are used instead of UNIX translation.\n\n04/21/04: Update: The FAT 'istat' output better handles Long FIle\nName entry\n\n04/21/04: Update: The FAT 'istat' output better handles Volume Label\nentry\n\n04/21/04: Update: Allowed the FAT volume label entry to be displayed\nwith 'ils'\n\n04/21/04: Update: Allowed the FAT volume label entry to be displayed\nwith 'fls'\n\n04/24/04: Update: 'dstat' on a FAT cluster now shows the cluster\naddress in addition to the sector address.\n\n04/24/04: Update: Added the cluster range to the FAT 'fsstat' output\n\n05/01/04: Update: Improved the FAT version autodetect code.  \n\n05/02/04: Update: Removed 'H' flag from 'icat'.\n\n05/02/04: Update: Changed all of the FS_FLAG_XXX variables in the\n  file system tools to constants that are specific to the usage\n  (NAME, DATA, META, FILE).  \n\n05/03/04: Update: fatfs_inode_walk now goes by sectors instead of clusters\n  to get more dentries from slack space.\n\n05/03/04: Bug Fix: The allocation status of FAT dentires was set only by\n  the flag and not the allocation status of the cluster it is located in.\n  (BUG: 947112)\n\n05/03/04: Update: Improved comments and variable names in FAT code\n\n05/03/04: Update: Added '-r' flag to 'icat' for deleted file recovery\n\n05/03/04: Update: Added RECOVERY flag to file_walk for deleted file\n  recovery\n\n05/03/04: Update: Added FAT file recovery.  \n\n05/03/04: Update: Removed '-H' flag from 'icat'.  Default is to \n  display holes.\n  \n05/03/04: Update: 'fls -r' will recurse down deleted directories in FAT\n\n05/03/04: Update: 'fsstat' reports FAT clusters that are marked as BAD\n\n05/03/04: Update: 'istat' for FAT now shows recovery clusters for \n  deleted files.\n\n05/04/04: Update:  Added output to 'fsstat' for FAT file systems by adding\n  a list of BAD sectors and improving the amount of layout information.  I\n  also changed some of the internal variables. \n\n05/08/04: Update: Removed addr_bsize from FS_INFO, moved block_frags \n  to FFS_INFO, modified dcat output only data unit size.\n\n05/20/04: Update: Added RECOVERY flag to 'ifind' so that it can find the\n  data units that are allocated to deleted files\n\n05/20/04: Update: Added icat recovery options to 'sorter'.\n\n05/20/04: Update: Improved the naming convention in sorter for the 'ils' \n  dead files.\n\n05/21/04: Update: Added outlook to sorter rules (from David Berger)\n\n05/27/04: Bug Fix: Added <linux/unistd.h> to mylseek.c so that it compiles\nwith Fedora Core 2 (Patch by Angus Marshall) (BUG: 961908).\n\n05/27/04: Update: Changed the letter with 'fls -l' for FIFO to 'p' \ninstead of 'f' (reported by Dave Henkewick).\n\n05/28/04: Update: Added '-u' flag to 'dcat' so that the data unit size\ncan be specified for raw, swap, and dls image types.\n\n05/28/04: Update: Changed the size  argument  of 'dcat' to be number of\ndata units instead of size in bytes (suggestion by Harald Katzer).\n\n\n---------------- VERSION 1.69 --------------\n03/06/04: Update: Fixed some memory leaks in ext2fs_close.  reported\n  by Paul Bakker.\n03/10/04: Bug Fix: If the '-s' flag was used with 'icat' on a EXT2FS\n  or FFS file system, then a large amount of extra data came out.\n  Reported by epsion.  (BUG: 913874)\n03/10/04: Bug Fix: One of the verbose outputs in ext2fs.c was being sent\n  to STDOUT instead of logfp. (BUG: 913875)\n04/14/04: Update: Added more data to fsstat output of FAT file system.\n04/15/04: Bug Fix:  The last sector of a FAT file system may not\n  be analyzed.  (BUG: 935976)\n04/16/04: Update: Added full support for swap and raw by making the\n standard files and functions for them instead of the hack in dcat.\n Suggested by (and initial patch by) Paul Baker.\n04/18/04: Update: Changed error messages in EXT2/3FS  code to be extXfs.\n04/18/04: Update: Updaged to version 4.09 of 'file'.  This will\n  help fix some of the problems people have had compiling it under\n  OS X 10.3.\n04/18/04: Update: Added compiling support for SFU 3.5 (Microsoft).  Patches\n  from an anonymous person.\n\n\n---------------- VERSION 1.68 --------------\n01/20/04: Bug Fix: FAT times were an hour too fast during daylight savings.\n  Now use mktime() instead of manual calculation.  Reported by Randall \n  Shane. (BUG: 880606)\n02/01/04: Update: 'hfind -i' now reports the header entry as an invalid\n  entry.  The first header row was ignored.\n02/20/04: Bug Fix: indirect block pointer blocks would not be identified by\n the ifind tool.  Reported by Knut Eckstein (BUG: 902709)\n03/01/04: Update: Added fs->seek_pos check to fs_read_random.\n\n---------------- VERSION 1.67 --------------\n11/15/03: Bug Fix: Added support for OS X 10.3 to src/makedefs. (BUG: 843029)\n11/16/03: Bug Fix: Mac partition tables could generate an error if there were\n  VOID-type partitions.   (BUG: 843366)\n11/21/03: Update: Changed NOABORT messages to verbose messages, so invalid\n  data is not printed during 'ifind' searches.\n11/30/03: Bug Fix: icat would not hide the 'holes' if '-h' was given because\n  the _UNALLOC flag was always being passed to file_walk.  (reported by\n  Knut Eckstein).  (BUG: 851873)\n11/30/03: Bug Fix: NTFS data_walk was not using _ALLOC and _UNALLOC flags \n  and other code that called it was not either.  (BUG: 851895)\n11/30/03: Bug Fix:  Not all needed commands were using _UNALLOC when they\n  called file_walk (although for most cases it did not matter because\n  sparse files would not be found in a directory for example). (Bug: 851897)  \n12/09/03: Bug Fix: FFS and EXT2FS code was using OFF_T type instead of\n  size_t for the size of the file. This could result in a file > 2GB\n  as being a negative size on some systems (BUG: 856957). \n12/26/03: Bug Fix: ffind would crash for root directory of FAT image.\n  Added NULL check and added a NULL name to fake root directory entry.\n  (BUG: 871219)\n01/05/04: Bug Fix: The clustcnt value for FAT was incorrectly calculated\n  and was too large for FAT12 and FAT16 by 32 sectors.  This could produce\n  extra entries in the 'fsstat' output when the FAT is dumped.  \n  (BUG: 871220)\n01/05/04: Bug Fix: ils, fls, and istat were not printing the full size\n  of files that are > 2GB.  (reported by Knut Eckstein) (BUG: 871457) \n01/05/04: Bug Fix: The EXT2FS and EXT3FS code was not using the\n  i_dir_acl value as the upper 32-bits of regular files that are\n  > 2GB (BUG:  871458)\n01/06/04: Mitigation: An error was reported where sorter would error\n  that icat was being passed a '-1' argument.  I can't find how that would\n  happen, so I added quotes to all arguments so that the next time it \n  occurs, the error is more useful (BUG: 845840).\n01/06/04: Update: Incorporated patch from Charles Seeger so that 'cc'\n  can be used and compile time warnings are fixed with Sun 'cc'.  \n01/06/04: Update: Upgraded file from v3.41 to v4.07\n\n\n---------------- VERSION 1.66 --------------\n09/02/03: Bug Fix: Would not compile under OpenBSD 3 because fs_tools.h\n  & mm_tools was missing a defined statement (reported by Randy - m0th_man)\nNOTE: Bugs now will have an entry into the Source Forge bug tracking\n  sytem.  \n10/13/03: Bug Fix: buffer was not being cleared between uses and length\n incorrectly set in NTFS resulted in false deleted file names being shown\n when the '-r' flag was given.  The extra entries were from the previous\n directory.  (BUG: 823057)\n10/13/03: Bug Fix: The results of 'sorter' varied depending on the version\n  of Perl and the system.   If the file output matched more than one,\n  sorter could not gaurantee which would match.  Therefore, results were\n  different for some files and some machines.  'sorter' now enforces the\n  ordering based on the order they are in the configuration file.  The\n  entries at the end of the file have priority over the first entries\n  (generic rules to specific rules).  (BUG: 823057)\n10/14/03: Update: 'mmls' prints 'MS LVM' with partition type 0x42 now.\n10/25/03: Bug Fix: NTFS could have a null pointer crash if the image\n  was very corrupt and $Data was not found for the MFT.  \n11/10/03: Bug Fix: NTFS 'ffind' would only report the file name and not\n  the attribute  name because the type and id were ignored.  ffind and\n  ntfs_dent were updated - found during NTFS keyword search test.\n  (Bug: 831579()\n11/12/03: Update: added support for Solaris x86 partition tables to 'mmls'\n11/12/03: Update: Modified the sparc data structure to add the correct \n  location of the 'sanity' magic value.\n11/15/03: Update: Added '-s' flag to 'icat' so that slack space is also\n  displayed.\n\n---------------- VERSION 1.65 --------------\n08/03/03: Bug Fix: 'sorter' now checks for inode values that are too\n  small to avoid 'icat' errors about invalid inode values.  \n08/19/03: Update: 'raw' is now a valid type for 'dcat'.\n08/21/03: Update: mactime and sorter look for perl5.6.0 first.\n08/21/03: Update: Removed NSRL support from 'sorter' until a better\n wany to identify the known good and known bad files is found\n08/21/03: Bug Fix: The file path replaces < and > with HTML \n  encoding for HTML output (ils names were not being shown)\n08/25/03: Update: Added 'nsrl.txt' describing why the NSRL functionality\n  was removed.\n08/27/03: Update: Improved code in 'mactime' to reduce warnings when\n  '-w' is used with Perl ('exists' checks on arrays).\n08/27/03: Update: Improved code in 'sorter' to reduce warnings when\n  '-w' is used with Perl (inode_int for NTFS).\n\n---------------- VERSION 1.64 --------------\n08/01/03: Docs Fix: The Sun VTOC was documented as Virtual TOC and it\n  should be Volume TOC (Jake @ UMASS).\n08/02/03: Bug Fix: Some compilers complained about verbose logging\n  assignment in 'mmls'  (Ralf Spenneberg).\n\n---------------- VERSION 1.63 --------------\n06/13/03; Update: Added 'mmtools' directory with 'dos' partitions\n  and 'mmls'.\n06/18/03: Update: Updated the documents in the 'doc' directory\n06/19/03: Update: Updated error message for EXT3FS magic check\n06/27/03: Update: Added slot & table number to mmls\n07/08/03: Update: Added mac support to mmtools\n07/11/03: Bug Fix: 'sorter' was not processing all unallocated meta\n  data structures because of a regexp error.  (reported by Jeff Reava)\n07/16/03: Update: Added support for FreeBSD5\n07/16/03: Update: Added BSD disk labels to mmtools\n07/28/03: Update: Relaxed requirements for DOS directory entries, the wtime\n  can be zero (reported by Adam Uccello).  \n07/30/03: Update: Added SUN VTOC to mmtools\n07/31/03: Update: Added NetBSD support (adam@monkeybyte.org)\n08/01/03: Update: Added more sanity checks to FAT so that it would not\n  try and process NTFS images that have the same MAGIC value\n\n---------------- VERSION 1.62 --------------\n04/11/03: Bug Fix: 'fsstat' for an FFS file system could report data \n  fragments in the last group that were larger than the maximum \n  fragment\n04/11/03: Bug Fix: 'ffs' allows the image to not be a multiple of the\n  block size.  A read error occurred when it tried to read the last\n  fragments since a whole block could not be read.\n04/15/03: Update: Added debug statements to FAT code.\n04/26/03: Update: Added verbose statements to FAT code\n04/26/03: Update: Added NOABORT flag to dls -s\n04/26/03: Update: Added stderr messages for errors that are not aborted\n  because of NOABORT\n05/27/03: Update: Added 'mask' field to FATFS_INFO structure and changed\n  code in fatfs.c to use it.\n05/27/03: Update: isdentry now checks the starting cluster to see if\n  it is a valid size.  \n05/27/03: Bug Fix: Added a sanitizer to 'sorter' to remove invalid chars\n  from the 'file' output and reduce the warnings from Perl.\n05/28/03: Bug Fix: Improved sanitize expression in 'sorter'\n05/28/03: Update: Added '-d' option to 'mactime' to allow output to be\n  given in comma delimited format for importing into a spread sheet or\n  other graphing tool\n06/09/03: Update: Added hourly summary / indexing to mactime\n06/09/03: Bug Fix: sorter would not allow linux-ext3 fstype\n\n\n---------------- VERSION 1.61 --------------\n02/05/03: Update: Started addition of image thumbnails to sorter\n03/05/03: Update: Updated 'file' to version 3.41\n03/16/03: Update: Added comments and NULL check to 'ifind'\n03/16/03: Bug Fix: Added a valid magic of 0 for MFT entries.  This was\n  found in an XP image.\n03/26/03: Bug Fix: fls would crash for an inode of 0 and a clock skew\n  was given.  fixed the bug in fls.c (debug help from Josep Homs)\n03/26/03: Update: Added more verbose comments to ntfs_dent.c.\n03/26/03: Bug Fix: 'ifind' for a path could return a result that was\n  shorter than the requested name (strncmp was used)\n03/26/03: Update: Short FAT names can be used in 'ifind -n' and \n  error messages were improved\n03/26/03: Bug Fix: A final NTFS Index Buffer was not always processed in \n  ntfs_dent.c, which resulted in files not being shown.  This was fixed\n  with debugging help from Matthew Shannon.\n03/27/03: Update: Added an 'index.html' for image thumbnails in sorter\n  and added a 'details' link from the thumbnail to the images.html file\n03/27/03: Update: 'sorter' can now take a directory inode to start \n  processing\n03/27/03: Update: added '-z' flag when running 'file' in 'sorter' so that\n  compressed file contents are reported\n03/27/03: Update: added '-i' flag to 'mactime' that creates a daily\n  summary of events\n03/27/03: Update: Added support for Version 2 of the NSRL in 'hfind'\n04/01/03: Update: Added support for Hash Keeper to 'hfind'\n04/01/03: Update: Added '-e' flag to 'hfind' for extended info \n  (currently hashkeeper only)\n\n\n---------------- VERSION 1.60 --------------\n10/31/02: Bug Fix: the unmounting status of EXT2FS in the 'fsstat' command\n  was not correct (reported by Stephane Denis).  \n11/24/02: Bug Fix: The -v argument was not allowed on istat or fls (Michael\n  Stone)\n11/24/02: Bug Fix: When doing an 'ifind' on a UNIX fs, it could abort if it\n  looked at an unallocated inode with invalid indirect block pointers.\n  This was fixed by adding a \"NOABORT\" flag to the walk code and adding\n  error checks in the file system code instead of relying on the fs_io\n  code.  (suggested by Micael Stone)\n11/26/02: Update: ifind has a '-n' argument that allows one to specify a\n  file name it and it searches to find the meta data structure for it\n  (suggested by William Salusky).\n11/26/02: Update: Now that there is a '-n' flag with 'ifind', the '-d'\n  flag was added to specify the data unit address.  The old syntax of\n  giving the data_unit at the end is no longer supported.  \n11/27/02: Update: Added sanity checks on meta data and data unit addresses\n  earlier in the code.\n12/12/02: Update: Added additional debug statements to NTFS code\n12/19/02: Update: Moved 'hash' directory to 'hashtools'\n12/19/02: Update: Started development of 'hfind'\n12/31/02: Update: Improved verbose debug statements to show full 64-bit \n  offsets\n01/02/03: Update: Finished development of 'hfind' with ability to update\n  for next version of NSRL (which may have a different format)\n01/05/03: Bug Fix: FFS and EXT2FS symbolic link destinations where not\n  properly NULL terminated and some extra chars were appended in 'fls'\n  (later reported by Thorsten Zachmann)\n01/06/03: Bug Fix: getu64() was not properly masking byte sizes and some\n  data was being lost.  This caused incorrect times to be displayed in some\n  NTFS files.\n01/06/03: Bug Fix: ifind reported incorrect ownership for some UNIX\n  file systems if the end fragments were allocated to a different file than\n  the first ones were.\n01/07/03: Update: Renamed the src/mactime directory to src/timeline.\n01/07/03: Update: Updated README and man pages for hfind and sorter\n01/12/03: Bug Fix: ntfs_mft_lookup was casting a 64-bit value to a 32-bit\n  variable.  This caused MFT Magic errors.  Reported and debugged by \n  Keven Murphy\n01/12/03: Update: Added verbose argument to 'fls'\n01/12/03: Bug Fix: '-V' argument to 'istat' was doing verbose instead of \n  version\n01/13/03: Update: Changed static sizes of OFF_T and DADDR_T in Linux \n  version to the actual 'off_t' and 'daddr_t' types\n01/23/03: Update: Changed use of strtok_r to strtok in ifind.c so that\n  Mac 10.1 could compile (Dave Goldsmith).\n01/28/03: Update: Improved code in 'hfind' and 'sorter' to handle\n  files with spaces in the path (Dave Goldsmith).\n\n---------------- VERSION 1.52 --------------\n09/24/02: Bug Fix: Memory leak in ntfs_dent_idxentry(), ntfs_find_file(),\n  and ntfs_dent_walk()\n09/24/02: Update: Removal of index sequences for index buffers is now\n  done using upd_off, which will allow for NTFS to move the structure in\n  the future.\n09/26/02: Update: Added create time for NTFS / STANDARD_INFO to \n  istat output.\n09/26/02: Update: Changed the method that the NTFS time is converted\n  to UNIX time.  Should be more efficient.\n10/09/02: Update: dcat error changed.\n10/02/02: Update: Includes a Beta version of 'sorter'\n\n\n---------------- VERSION 1.51 --------------\n09/10/02: Bug Fix: Fixed a design bug that would not allow attribute \n  lists in $MFT.  This bug would generate an error that complained about\n  an invalid MFT entry in attribute list.  \n09/10/02: Update: The size of files and directories is now calculated\n  after each time proc_attrseq() is called so that it is more up to date\n  when dealing with attribute lists.  The size has the sizes of all\n  $Data, $IDX_ROOT, and $IDX_ALLOC streams. \n09/10/02: Update: The maxinum number of MFT entries is now calculated\n  each time an MFT entry is processed while loading the MFT.  This \n  allows us to reflect what the maximum possible MFT entry is at that\n  given point based on how many attribute lists have been processed.\n09/10/02: Update: Added file version 3.39 to distro (bigger magic files) \n  (Salusky)\n09/10/02: Bug Fix: fs_data was wasting memory when it was allocated\n09/10/02: Update: added a fs_data_alloc() function\n09/12/02: Bug Fix: Do not give an error if an attribute list of an\n  unallocated file points to an MFT that no longer claims it is a \n  member of the list.\n09/12/02: Update: No longer need version to remove update sequence\n  values from on-disk buffers\n09/19/02: Bug Fix: fixed memory leak in ntfs_load_ver() \n09/19/02: Bug Fix: Update sequence errors were displayed because of a\n  bug that occurred when an MFT entry crossed a run in $MFT.  Only occurred\n  with 512-byte clusters and an odd number of clusters in a run.\n09/19/02: Update: New argument to ils, istat, and fls that allows user to\n  specify a time skew in seconds of the compromised system.  Originated\n  from discussion at DFRWS II.  \n09/19/02: Update: Added '-h' argument to mactime to display header info \n\n---------------- VERSION 1.50 --------------\n\n04/21/02: icat now displays idxroot attribute for NTFS directories\n04/21/02: fs_dent_print functions now are passed the FS_DATA structure \n  instead of the extra inode and name strings.  (NTFS)\n04/21/02: fs_dent_print functions display alternate data stream size instead\n of the default data size (NTFS)\n04/24/02: Fixed bug in istat that displayed too many fragments with ffs images \n04/24/02: Fixed bug in istat that did not display sparse files correctly\n04/24/02: fsstat of FFS images now identifies the fragments at the \n  beginning of cyl groups as data fragments.\n04/26/02: Fixed bug in ext2fs_dent_parse_block that did not advance the\n  directory entry pointer far enough each time\n04/26/02: Fixed bug in ext2fs_dent_parse_block so that gave an error if\n  a file name was exactly 255 chars\n04/29/02: Removed the getX functions from get.c as they are now macros\n05/11/02: Added support for lowercase flag in FAT\n05/11/02: Added support for sequence values (NTFS)\n05/13/02: Added FS_FLAG_META for FAT\n05/13/02: Changed ifind so that it looks the block up to identify if it is\n  a meta data block when an inode can not be found\n05/13/02: Added a conditional to ifind so that it handles sparse files better\n05/19/02: Changed icat so that the default attribute type is set in the\n  file_walk function\n05/20/02: ils and dls now use boundary inode & block values if too large\n  or small are given\n05/21/02: istat now displays all NTFS times\n05/21/02: Created functions to just display date and time\n05/24/02: moved istat functionality to the specific file system file\n05/25/02: added linux-ext3 flag, but no new features\n05/25/02: Added sha1 (so Autopsy can use the NIST SW Database)\n05/26/02: Fixed bug with FAT that did not return all slack space on file_walk\n05/26/02: Added '-s' flag to dls to extract slack space of FAT and NTFS\n06/07/02: fixed _timezone variable so correct times are shown in CYGWIN\n06/11/02: *_copy_inode now sets the flags for the inode \n06/11/02: fixed bug in mactimes that displayed a duplicate entry with time \n  because of header entries in body file\n06/12/02: Added ntfs.README doc\n06/16/02: Added a comment to file Makefile to make it easier to compile for\n  an IR CD.\n06/18/02: Fixed NTFS bug that showed ADS when only deleted files were supposed\n  to be shown (when ADS in directory)\n06/19/02: added the day of the week to the mactime output (Tan)\n07/09/02: Fixed bug that added extra chars to end of symlink destination\n07/17/02: 1.50 Released \n\n\n\n---------------- VERSION 1.00 --------------\n- Integrated TCT-1.09 and TCTUTILs-1.01\n- Fixed bug in bcat if size is not given with type of swap.\n- Added platform indep by including the structures of each file system type\n- Added flags for large file support under linux\n- blockcalc was off by 1 if calculated using the raw block number and\nnot the one that lazarus spits out (which start at 1)\n- Changed the inode_walk and block_walk functions slightly to return a\nvalue so that a walk can be ended in the middle of it.\n- FAT support added\n- Improved ifind to better handle fragments\n- '-z' flag to fls and istat now use the time zone string instead of \ninteger value.\n- no longer prepend / in _dent\n- verify that '-m' directory in fls ends with a '/' \n- identify the destination of sym links\n- fsstat tool added\n- fixed caching bug with FAT12 when the value overlapped cache entries\n- added mactime\n- removed the <inode> value in fls when printing mac format (inode is now printed in mactime)\n- renamed src/misc directory to src/hash (it only has md5 and will have sha)\n- renamed aux directory to misc (Windows doesn't allow aux as a name ??)\n- Added support for Cygwin\n- Use the flags in super block of EXT2FS to identify v1 or v2\n- removed file system types of linux1 and linux2 and linux\n- added file system type of linux-ext2 (as ext3 is becoming more popular)\n- bug in file command that reported seek error for object files and STDIN\n\n\n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/README-win32.txt",
    "content": "                          The Sleuth Kit\n                        Windows Executables\n\n                http://www.sleuthkit.org/sleuthkit\n\n               Brian Carrier [carrier@sleuthkit.org]\n\n                     Last Updated: July 2012\n\n\n======================================================================\n\nThis zip file contains the Microsoft Windows executables for The Sleuth\nKit.  The full source code (including Visual Studio Solution files) and \ndocumentation can be downloaded from:\n\nhttp://www.sleuthkit.org\n\nThese are distributed under the IBM Public License and the Common \nPublic License, which can be found in the licenses folder. \n\n\nNOTES\n\nThe dll files in the zip file are required to run the executables. They\nmust be either in the same directory as the executables or in the path.\n\nThere have been reports of the exe files not running on some systems\nand they give the error \"The system cannot execute the specified program\".\nThis occurs because the system can't find the needed dll files. Installing\nthe \"Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)\" seems\nto fix the problem.  It can be downloaded from Microsoft:\n\nhttp://www.microsoft.com/downloads/en/confirmation.aspx?FamilyID=A5C84275-3B97-4AB7-A40D-3802B2AF5FC2&displaylang=en\n\n\nmactime.pl requires a Windows port of Perl to be installed. If you have \nthe \".pl\" extension associated with Perl, you should be able to run\n\"mactime.pl\" from the command line. Otherwise, you may need to run it\nas \"perl mactime.pl\".  Examples of Windows ports of Perl include:\n- ActivePerl (http://www.activestate.com/activeperl/)\n- Strawberry Perl (http://strawberryperl.com/)\n\n\nCURRENT LIMITATIONS\n\nThe tools do not currently support globbing, which means that you \ncannot use 'fls img.*' on a split image.  Windows does not automatically\nexpand the '*' to all file names.  However, most split images can now\nbe used in The Sleuth Kit by simply specifying the first segment's path.\n\nThese programs can be run on a live system, if you use the \n\\\\.\\PhysicalDrive0 syntax.  Note though, that you may get errors or the\nfile system type may not be detected because the data being read is out \nof sync with cached versions of the data.  \n\nUnicode characters are not always properly displayed in the command\nshell.\n\nThe AFF image formats are not supported. \n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/README.txt",
    "content": "[![Build Status](https://travis-ci.org/sleuthkit/sleuthkit.svg?branch=develop)](https://travis-ci.org/sleuthkit/sleuthkit)\n\n[![Build status](https://ci.appveyor.com/api/projects/status/8f7ljj8s2lh5sqfv?svg=true)](https://ci.appveyor.com/project/bcarrier/sleuthkit)\n\n# [The Sleuth Kit](http://www.sleuthkit.org/sleuthkit)\n\n## INTRODUCTION\nThe Sleuth Kit is an open source forensic toolkit for analyzing\nMicrosoft and UNIX file systems and disks.  The Sleuth Kit enables\ninvestigators to identify and recover evidence from images acquired\nduring incident response or from live systems.  The Sleuth Kit is\nopen source, which allows investigators to verify the actions of\nthe tool or customize it to specific needs.\n\nThe Sleuth Kit uses code from the file system analysis tools of\nThe Coroner's Toolkit (TCT) by Wietse Venema and Dan Farmer.  The\nTCT code was modified for platform independence.  In addition,\nsupport was added for the NTFS (see [wiki/ntfs](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes)) \nand FAT (see [wiki/fat](http://wiki.sleuthkit.org/index.php?title=FAT_Implementation_Notes)) file systems.  Previously, The Sleuth Kit was\ncalled The @stake Sleuth Kit (TASK).  The Sleuth Kit is now independent\nof any commercial or academic organizations.\n\nIt is recommended that these command line tools can be used with\nthe Autopsy Forensic Browser.  Autopsy, (http://www.sleuthkit.org/autopsy),\nis a graphical interface to the tools of The Sleuth Kit and automates\nmany of the procedures and provides features such as image searching\nand MD5 image integrity checks.\n\nAs with any investigation tool, any results found with The Sleuth\nKit should be be recreated with a second tool to verify the data.\n\n## OVERVIEW\nThe Sleuth Kit allows one to analyze a disk or file system image\ncreated by 'dd', or a similar application that creates a raw image.\nThese tools are low-level and each performs a single task.  When\nused together, they can perform a full analysis.  For a more detailed\ndescription of these tools, refer to [wiki/filesystem](http://wiki.sleuthkit.org/index.php?title=TSK_Tool_Overview). \nThe tools are briefly described in a file system layered approach.  Each\ntool name begins with a letter that is assigned to the layer.  \n\n### File System Layer:\nA disk contains one or more partitions (or slices).  Each of these\npartitions contain a file system.  Examples of file systems include\nthe Berkeley Fast File System (FFS), Extended 2 File System (EXT2FS),\nFile Allocation Table (FAT), and New Technologies File System (NTFS).\n\nThe fsstat tool displays file system details in an ASCII format.\nExamples of data in this display include volume name, last mounting\ntime, and the details about each \"group\" in UNIX file systems.\n\n### Content Layer (block):\nThe content layer of a file system contains the actual file content,\nor data.  Data is stored in large chunks, with names such as blocks,\nfragments, and clusters.  All tools in this layer begin with the letters\n'blk'.  \n\nThe blkcat tool can be used to display the contents of a specific unit of\nthe file system (similar to what 'dd' can do with a few arguments).\nThe unit size is file system dependent.  The 'blkls' tool displays the\ncontents of all unallocated units of a file system, resulting in a\nstream of bytes of deleted content.  The output can be searched for\ndeleted file content.  The 'blkcalc' program allows one to identify the\nunit location in the original image of a unit in the 'blkls' generated\nimage.\n\nA new feature of The Sleuth Kit from TCT is the '-l' argument to\n'blkls' (or 'unrm' in TCT).  This argument lists the details for data\nunits, similar to the 'ils' command.  The 'blkstat' tool displays\nthe statistics of a specific data unit (including allocation status\nand group number).\n\n### Metadata Layer (inode):\nThe metadata layer describes a file or directory.  This layer contains\ndescriptive data such as dates and size as well as the addresses of the\ndata units.  This layer describes the file in terms that the computer\ncan process efficiently.   The structures that the data is stored in\nhave names such as inode and directory entry.  All tools in this layer\nbegin with an 'i'.  \n\nThe 'ils' program lists some values of the metadata structures.\nBy default, it will only list the unallocated ones.  The 'istat'\ndisplays metadata information in an ASCII format about a specific\nstructure.  New to The Sleuth Kit is that 'istat' will display the\ndestination of symbolic links.  The 'icat' function displays the\ncontents of the data units allocated to the metadata structure\n(similar to the UNIX cat(1) command).  The 'ifind' tool will identify\nwhich metadata structure has allocated a given content unit or\nfile name.\n\nRefer to the [ntfs wiki](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes) \nfor information on addressing metadata attributes in NTFS.\n\n### Human Interface Layer (file):\nThe human interface layer allows one to interact with files in a\nmanner that is more convenient than directly with the metadata\nlayer.  In some operating systems there are separate structures for\nthe metadata and human interface layers while others combine them.\nAll tools in this layer begin with the letter 'f'.  \n\nThe 'fls' program lists file and directory names.  This tool will\ndisplay the names of deleted files as well.  The 'ffind' program will\nidentify the name of the file that has allocated a given metadata\nstructure.  With some file systems, deleted files will be identified.\n\n#### Time Line Generation\nTime lines are useful to quickly get a picture of file activity.\nUsing The Sleuth Kit a time line of file MAC times can be easily\nmade.  The mactime (TCT) program takes as input the 'body' file\nthat was generated by fls and ils.  To get data on allocated and\nunallocated file names, use 'fls -rm dir' and for unallocated inodes\nuse 'ils -m'.  Note that the behavior of these tools are different\nthan in TCT.  For more information, refer to [wiki/mactime](http://wiki.sleuthkit.org/index.php?title=Mactime).\n\n\n#### Hash Databases\nHash databases are used to quickly identify if a file is known.  The\nMD5 or SHA-1 hash of a file is taken and a database is used to identify\nif it has been seen before.  This allows identification to occur even\nif a file has been renamed.\n\nThe Sleuth Kit includes the 'md5' and 'sha1' tools to generate\nhashes of files and other data.\n\nAlso included is the 'hfind' tool.  The 'hfind' tool allows one to create\nan index of a hash database and perform quick lookups using a binary\nsearch algorithm.  The 'hfind' tool can perform lookups on the NIST\nNational Software Reference Library (NSRL) (www.nsrl.nist.gov) and\nfiles created from the 'md5' or 'md5sum' command.   Refer to the \n[wiki/hfind](http://wiki.sleuthkit.org/index.php?title=Hfind) file for more details.  \n\n#### File Type Categories\nDifferent types of files typically have different internal structure.\nThe 'file' command comes with most versions of UNIX and a copy is\nalso distributed with The Sleuth Kit.  This is used to identify\nthe type of file or other data regardless of its name and extension.\nIt can even be used on a given data unit to help identify what file\nused that unit for storage.  Note that the 'file' command typically\nuses data in the first bytes of a file so it may not be able to\nidentify a file type based on the  middle blocks or clusters.\n\nThe 'sorter' program in The Sleuth Kit will use other Sleuth Kit\ntools to sort the files in a file system image into categories.\nThe categories are based on rule sets in configuration files.  The\n'sorter' tool will also use hash databases to flag known bad files\nand ignore known good files.  Refer to the [wiki/sorter](http://wiki.sleuthkit.org/index.php?title=Sorter)\nfile for more details.\n\n\n## LICENSE\nThere are a variety of licenses used in TSK based on where they\nwere first developed.  The licenses are located in the [licenses\ndirectory](https://github.com/sleuthkit/sleuthkit/tree/develop/licenses).\n\n- The file system tools (in the\n[tools/fstools](https://github.com/sleuthkit/sleuthkit/tree/develop/tools/fstools)\ndirectory) are released under the IBM open source license and Common\nPublic License.\n- srch_strings and fiwalk are released under the GNU Public License\n- Other tools in the tools directory are Common Public License\n- The modifications to 'mactime' from the original 'mactime' in TCT\nand 'mac-daddy' are released under the Common Public License.\n\nThe library uses utilities that were released under MIT and BSD 3-clause. \n\n\n## INSTALL\nFor installation instructions, refer to the INSTALL.txt document.\n\n## OTHER DOCS\nThe [wiki](http://wiki.sleuthkit.org/index.php?title=Main_Page) contains documents that \ndescribe the provided tools in more detail.  The Sleuth Kit Informer is a newsletter that contains\nnew documentation and articles.\n\n> www.sleuthkit.org/informer/\n\n## MAILING LIST\nMailing lists exist on SourceForge, for both users and a low-volume\nannouncements list.\n\n> http://sourceforge.net/mail/?group_id=55685\n\nBrian Carrier\n\ncarrier at sleuthkit dot org\n \n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/bin/mactime.pl",
    "content": "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 Rob Lee.\n#\n# It takes as input data from either 'ils -m' or 'fls -m' (from The Sleuth\n# Kit) or 'mac-robber'.\n# Based on the dates as arguments given, the data is sorted by and\n# printed.\n#\n# The Sleuth Kit\n# Brian Carrier [carrier <at> sleuthkit [dot] org]\n# Copyright (c) 2003-2012 Brian Carrier.  All rights reserved\n#\n# TASK\n# Copyright (c) 2002 Brian Carrier, @stake Inc.  All rights reserved\n#\n#\n# The modifications to the original mactime are distributed under\n# the Common Public License 1.0\n#\n#\n# Copyright 1999 by Dan Farmer.  All rights reserved.  Some individual\n# files may be covered by other copyrights (this will be noted in the\n# file itself.)\n#\n# Redistribution and use in source and binary forms are permitted\n# provided that this entire copyright notice is duplicated in all such\n# copies.\n#\n# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED\n# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE.\n#\n# IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,\n# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n# (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR\n# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\n# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\n# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n#\n\nuse POSIX;\nuse strict;\n\nmy $debug = 0;\n\n# %month_to_digit = (\"Jan\", 1, \"Feb\", 2, \"Mar\", 3, \"Apr\", 4, \"May\", 5, \"Jun\", 6,\n#                    \"Jul\", 7, \"Aug\", 8, \"Sep\", 9, \"Oct\", 10, \"Nov\", 11, \"Dec\", 12);\nmy %digit_to_month = (\n    \"01\", \"Jan\", \"02\", \"Feb\", \"03\", \"Mar\", \"04\", \"Apr\",\n    \"05\", \"May\", \"06\", \"Jun\", \"07\", \"Jul\", \"08\", \"Aug\",\n    \"09\", \"Sep\", \"10\", \"Oct\", \"11\", \"Nov\", \"12\", \"Dec\"\n);\nmy %digit_to_day = (\n    \"0\", \"Sun\", \"1\", \"Mon\", \"2\", \"Tue\", \"3\", \"Wed\",\n    \"4\", \"Thu\", \"5\", \"Fri\", \"6\", \"Sat\"\n);\n\nsub usage {\n    print <<EOF;\nmactime [-b body_file] [-p password_file] [-g group_file] [-i day|hour idx_file] [-d] [-h] [-V] [-y] [-z TIME_ZONE] [DATE]\n\t\t-b: Specifies the body file location, else STDIN is used\n\t\t-d: Output in comma delimited format\n\t\t-h: Display a header with session information\n\t\t-i [day | hour] file: Specifies the index file with a summary of results\n\t\t-y: Dates are displayed in ISO 8601 format\n\t\t-m: Dates have month as number instead of word (does not work with -y)\n\t\t-z: Specify the timezone the data came from (in the local system format) (does not work with -y)\n\t\t-g: Specifies the group file location, else GIDs are used\n\t\t-p: Specifies the password file location, else UIDs are used\n\t\t-V: Prints the version to STDOUT\n\t\t[DATE]: starting date (yyyy-mm-dd) or range (yyyy-mm-dd..yyyy-mm-dd) \n\t\t[DATE]: date with time (yyyy-mm-ddThh:mm:ss), using with range one or both can have time\nEOF\n    exit(1);\n}\n\nsub version {\n    print \"The Sleuth Kit ver $VER\\n\";\n}\n\nmy $BODY       = \"\";\nmy $GROUP      = \"\";\nmy $PASSWD     = \"\";\nmy $TIME       = \"\";\nmy $INDEX      = \"\";            # File name of index\nmy $INDEX_DAY  = 1;             # Daily index (for $INDEX_TYPE)\nmy $INDEX_HOUR = 2;\nmy $INDEX_TYPE = $INDEX_DAY;    # Saved to type of index\nmy $COMMA      = 0;             # Comma delimited output\n\nmy $iso8601 = 0;\nmy $month_num  = 0;\nmy $header     = 0;\n\nmy $in_seconds  = 0;\nmy $out_seconds = 0;\nmy %timestr2macstr;\nmy %file2other;\n\nmy %gid2names = ();\nmy %uid2names = ();\n\nmy $_HAS_DATETIME_TIMEZONE = 0;\n\neval \"use DateTime::TimeZone\";\nif ($@) {\n    $_HAS_DATETIME_TIMEZONE = 0;\n} else {\n    $_HAS_DATETIME_TIMEZONE = 1;\n}\n\nsub get_timezone_list() {\n    my @t_list;\n    if ( ! $_HAS_DATETIME_TIMEZONE ) {\n       return @t_list;\n    }\n\n    foreach ( DateTime::TimeZone->all_names() ) {\n        push( @t_list, $_ );\n    }\n    foreach( keys( %{DateTime::TimeZone->links()})  ) {\n        push( @t_list, $_ );\n    }\n\n    return sort { $a cmp $b } @t_list;\n}\n\nusage() if (scalar(@ARGV) == 0);\n\nwhile ((scalar(@ARGV) > 0) && (($_ = $ARGV[0]) =~ /^-(.)(.*)/)) {\n\n    # Body File\n    if (/^-b$/) {\n        shift(@ARGV);\n        if (defined $ARGV[0]) {\n            $BODY = $ARGV[0];\n        }\n        else {\n            print \"-b requires body file argument\\n\";\n        }\n    }\n    elsif (/^-d$/) {\n        $COMMA = 1;\n    }\n\n    # Group File\n    elsif (/^-g$/) {\n        shift(@ARGV);\n        if (defined $ARGV[0]) {\n            &'load_group_info($ARGV[0]);\n            $GROUP = $ARGV[0];\n        }\n        else {\n            print \"-g requires group file argument\\n\";\n            usage();\n        }\n    }\n\n    # Password File\n    elsif (/^-p$/) {\n        shift(@ARGV);\n        if (defined $ARGV[0]) {\n            &'load_passwd_info($ARGV[0]);\n            $PASSWD = $ARGV[0];\n        }\n        else {\n            print \"-p requires password file argument\\n\";\n            usage();\n        }\n    }\n    elsif (/^-h$/) {\n        $header = 1;\n    }\n\n    # Index File\n    elsif (/^-i$/) {\n        shift(@ARGV);\n\n        if (defined $ARGV[0]) {\n\n            if ($INDEX ne \"\") {\n                print \"Only one -i argument can be supplied\\n\";\n                usage();\n            }\n\n            # Find out what type\n            if ($ARGV[0] eq \"day\") {\n                $INDEX_TYPE = $INDEX_DAY;\n            }\n            elsif ($ARGV[0] eq \"hour\") {\n                $INDEX_TYPE = $INDEX_HOUR;\n            }\n            shift(@ARGV);\n            unless (defined $ARGV[0]) {\n                print \"-i requires index file argument\\n\";\n                usage();\n            }\n            $INDEX = $ARGV[0];\n        }\n        else {\n            print \"-i requires index file argument and type\\n\";\n            usage();\n        }\n        open(INDEX, \">$INDEX\") or die \"Can not open $INDEX\";\n    }\n    elsif (/^-V$/) {\n        version();\n        exit(0);\n    }\n    elsif (/^-m$/) {\n        $month_num = 1;\n    }\n    elsif (/^-y$/) {\n        $iso8601 = 1;\n    }\n    elsif (/^-z$/) {\n        shift(@ARGV);\n        if (defined $ARGV[0]) {\n            my $tz = \"$ARGV[0]\";\n\n            if ($tz =~ m/^list$/i) {\n                if ($_HAS_DATETIME_TIMEZONE) {\n                    my $txt  = \"\n-----------------------------------\n        TIMEZONE LIST\n-----------------------------------\\n\";\n                    foreach ( get_timezone_list() ) {\n                        $txt .= $_ . \"\\n\";\n                    }\n                    print( $txt );\n                }\n                else {\n                    print \"DateTime module not loaded -- cannot list timezones\\n\";\n                }\n                exit(0);\n            }\n            # validate the string if we have DateTime module\n            elsif ($_HAS_DATETIME_TIMEZONE) {\n                my $realtz = 0;\n                foreach ( get_timezone_list() ) {\n                    if ($tz =~ m/^$_$/i) {\n                        $realtz = $_;\n                        last;\n                    }\n                }\n                if ($realtz) {\n                    $ENV{TZ} = $realtz;\n                }\n                else {\n                    print \"invalid timezone provided. Use '-z list' to list valid timezones.\\n\";\n                    usage();\n                }\n            }\n            # blindly take it otherwise\n            else {\n                $ENV{TZ} = $tz;\n            }      \n        }\n        else {\n            print \"-z requires the time zone argument\\n\";\n            usage();\n        }\n    }\n    else {\n        print \"Unknown option: $_\\n\";\n        usage();\n    }\n    shift(@ARGV);\n}\n\n# Was the time given\nif (defined $ARGV[0]) {\n    my $t_in;\n    my $t_out;\n\n    $TIME = $ARGV[0];\n    if ($ARGV[0] =~ /\\.\\./) {\n        ($t_in, $t_out) = split(/\\.\\./, $ARGV[0]);\n    }\n    else {\n        $t_in  = $ARGV[0];\n        $t_out = 0;\n    }\n    $in_seconds = parse_isodate($t_in);\n    die \"Invalid Date: $t_in\\n\" if ($in_seconds < 0);\n\n    if ($t_out) {\n        $out_seconds = parse_isodate($t_out);\n        die \"Invalid Date: $t_out\\n\" if ($out_seconds < 0);\n    }\n    else {\n        $out_seconds = 0;\n    }\n}\nelse {\n    $in_seconds  = 0;\n    $out_seconds = 0;\n}\n\n# Print header info\nprint_header() if ($header == 1);\n\n# Print the index header\nif ($INDEX ne \"\") {\n    my $time_str = \"\";\n    if ($INDEX_TYPE == $INDEX_DAY) {\n        $time_str = \"Daily\";\n    }\n    else {\n        $time_str = \"Hourly\";\n    }\n    if ($BODY ne \"\") {\n        print INDEX \"$time_str Summary for Timeline of $BODY\\n\\n\";\n    }\n    else {\n        print INDEX \"$time_str Summary for Timeline of STDIN\\n\\n\";\n    }\n}\n\nread_body();\n\nprint_tl();\n\n################ SUBROUTINES ##################\n\n#convert yyyy-mm-dd string to Unix date\nsub parse_isodate {\n    my $iso_date = shift;\n\n    my $sec  = 0;\n    my $min  = 0;\n    my $hour = 0;\n    my $wday = 0;\n    my $yday = 0;\n    if ($iso_date =~ /^(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)$/) {\n        return mktime($sec, $min, $hour, $3, $2 - 1, $1 - 1900, $wday, $yday);\n    }\n    elsif ($iso_date =~ /^(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)$/) {\n        return mktime($6, $5, $4, $3, $2 - 1, $1 - 1900, $wday, $yday);\n    }\n    else {\n        return -1;\n    }\n}\n\n# Read the body file from the BODY variable\nsub read_body {\n\n    # Read the body file from STDIN or the -b specified body file\n    if ($BODY ne \"\") {\n        open(BODY, \"<$BODY\") or die \"Can't open $BODY\";\n    }\n    else {\n        open(BODY, \"<&STDIN\") or die \"Can't dup STDIN\";\n    }\n\n    while (<BODY>) {\n        next if ((/^\\#/) || (/^\\s+$/));\n\n        chomp;\n\n        my (\n            $tmp1,     $file,     $st_ino,    $st_ls,\n            $st_uid,   $st_gid,   $st_size,   $st_atime,\n            $st_mtime, $st_ctime, $st_crtime, $tmp2\n          )\n          = &tm_split($_);\n\n        # Sanity check so that we ignore the header entries\n        next unless ((defined $st_ino)    && ($st_ino    =~ /[\\d-]+/));\n        next unless ((defined $st_uid)    && ($st_uid    =~ /\\d+/));\n        next unless ((defined $st_gid)    && ($st_gid    =~ /\\d+/));\n        next unless ((defined $st_size)   && ($st_size    =~ /\\d+/));\n        next unless ((defined $st_mtime)  && ($st_mtime  =~ /\\d+/));\n        next unless ((defined $st_atime)  && ($st_atime  =~ /\\d+/));\n        next unless ((defined $st_ctime)  && ($st_ctime  =~ /\\d+/));\n        next unless ((defined $st_crtime) && ($st_crtime =~ /\\d+/));\n\n        # we need *some* value in mactimes!\n        next if (!$st_atime && !$st_mtime && !$st_ctime && !$st_crtime);\n\n        # Skip if these are all too early\n        next\n          if ( ($st_mtime < $in_seconds)\n            && ($st_atime < $in_seconds)\n            && ($st_ctime < $in_seconds)\n            && ($st_crtime < $in_seconds));\n\n        # add leading zeros to timestamps because we will later sort\n        # these using a string-based comparison\n        $st_mtime  = sprintf(\"%.10d\", $st_mtime);\n        $st_atime  = sprintf(\"%.10d\", $st_atime);\n        $st_ctime  = sprintf(\"%.10d\", $st_ctime);\n        $st_crtime = sprintf(\"%.10d\", $st_crtime);\n\n        # Put all the times in one big array along with the inode and\n        # name (they are used in the final sorting)\n\n        # If the date on the file is too old, don't put it in the array\n        my $post = \",$st_ino,$file\";\n\n        if ($out_seconds) {\n            $timestr2macstr{\"$st_mtime$post\"} .= \"m\"\n              if (\n                   ($st_mtime >= $in_seconds)\n                && ($st_mtime < $out_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_mtime$post\"}))\n                    || ($timestr2macstr{\"$st_mtime$post\"} !~ /m/))\n              );\n\n            $timestr2macstr{\"$st_atime$post\"} .= \"a\"\n              if (\n                   ($st_atime >= $in_seconds)\n                && ($st_atime < $out_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_atime$post\"}))\n                    || ($timestr2macstr{\"$st_atime$post\"} !~ /a/))\n              );\n\n            $timestr2macstr{\"$st_ctime$post\"} .= \"c\"\n              if (\n                   ($st_ctime >= $in_seconds)\n                && ($st_ctime < $out_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_ctime$post\"}))\n                    || ($timestr2macstr{\"$st_ctime$post\"} !~ /c/))\n              );\n\n            $timestr2macstr{\"$st_crtime$post\"} .= \"b\"\n              if (\n                   ($st_crtime >= $in_seconds)\n                && ($st_crtime < $out_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_crtime$post\"}))\n                    || ($timestr2macstr{\"$st_crtime$post\"} !~ /b/))\n              );\n        }\n        else {\n            $timestr2macstr{\"$st_mtime$post\"} .= \"m\"\n              if (\n                ($st_mtime >= $in_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_mtime$post\"}))\n                    || ($timestr2macstr{\"$st_mtime$post\"} !~ /m/))\n              );\n\n            $timestr2macstr{\"$st_atime$post\"} .= \"a\"\n              if (\n                ($st_atime >= $in_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_atime$post\"}))\n                    || ($timestr2macstr{\"$st_atime$post\"} !~ /a/))\n              );\n\n            $timestr2macstr{\"$st_ctime$post\"} .= \"c\"\n              if (\n                ($st_ctime >= $in_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_ctime$post\"}))\n                    || ($timestr2macstr{\"$st_ctime$post\"} !~ /c/))\n              );\n\n            $timestr2macstr{\"$st_crtime$post\"} .= \"b\"\n              if (\n                ($st_crtime >= $in_seconds)\n                && (   (!(exists $timestr2macstr{\"$st_crtime$post\"}))\n                    || ($timestr2macstr{\"$st_crtime$post\"} !~ /b/))\n              );\n        }\n\n        # if the UID or GID is not in the array then add it.\n        # these are filled if the -p or -g options are given\n        $uid2names{$st_uid} = $st_uid\n          unless (defined $uid2names{$st_uid});\n        $gid2names{$st_gid} = $st_gid\n          unless (defined $gid2names{$st_gid});\n\n        #\n        # put /'s between multiple UID/GIDs\n        #\n        $uid2names{$st_uid} =~ s@\\s@/@g;\n        $gid2names{$st_gid} =~ s@\\s@/@g;\n\n        $file2other{$file} =\n          \"$st_ls:$uid2names{$st_uid}:$gid2names{$st_gid}:$st_size\";\n    }\n\n    close BODY;\n}    # end of read_body\n\nsub print_header {\n    return if ($header == 0);\n\n    print \"The Sleuth Kit mactime Timeline\\n\";\n\n    print \"Input Source: \";\n    if ($BODY eq \"\") {\n        print \"STDIN\\n\";\n    }\n    else {\n        print \"$BODY\\n\";\n    }\n\n    print \"Time: $TIME\\t\\t\" if ($TIME ne \"\");\n\n    if ($ENV{TZ} eq \"\") {\n        print \"\\n\";\n    }\n    else {\n        print \"Timezone: $ENV{TZ}\\n\";\n    }\n\n    print \"passwd File: $PASSWD\" if ($PASSWD ne \"\");\n    if ($GROUP ne \"\") {\n        print \"\\t\" if ($PASSWD ne \"\");\n        print \"group File: $GROUP\";\n    }\n    print \"\\n\" if (($PASSWD ne \"\") || ($GROUP ne \"\"));\n\n    print \"\\n\";\n}\n\n#\n# Print the time line\n#\nsub print_tl {\n\n    my $prev_day        = \"\";   # has the format of 'day day_week mon year'\n    my $prev_hour       = \"\";   # has just the hour and is used for hourly index\n    my $prev_time       = 0;\n    my $prev_cnt        = 0;\n    my $old_date_string = \"\";\n\n    my $delim = \":\";\n    if ($COMMA != 0) {\n        print \"Date,Size,Type,Mode,UID,GID,Meta,File Name\\n\";\n        $delim = \",\";\n    }\n\n    # Cycle through the files and print them in sorted order.\n    # Note that we sort using a string comparison because the keys\n    # also contain the inode and file name\n    for my $key (sort { $a cmp $b } keys %timestr2macstr) {\n        my $time;\n        my $inode;\n        my $file;\n\n        if ($key =~ /^(\\d+),([\\d-]+),(.*)$/) {\n            $time  = $1;\n            $inode = $2;\n            $file  = $3;\n        }\n        else {\n            next;\n        }\n\n        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst);\n        if ($iso8601) {\n            ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =\n              gmtime($time);\n        }\n        else {\n            ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =\n              localtime($time);\n        }\n\n        # the month here is 0-11, not 1-12, like what we want\n        $mon++;\n\n        print\n\"\\t($sec,$min,$hour,MDay: $mday,M: $mon,$year,$wday,$yday,$isdst) = ($time)\\n\"\n          if $debug;\n\n        #\n        # cosmetic change to make it look like unix dates\n        #\n        $mon  = \"0$mon\"  if $mon < 10;\n        $mday = \"0$mday\" if $mday < 10;\n        $hour = \"0$hour\" if $hour < 10;\n        $min  = \"0$min\"  if $min < 10;\n        $sec  = \"0$sec\"  if $sec < 10;\n\n        my $yeart = $year + 1900;\n\n        #  How do we print the date?\n        #\n        my $date_string;\n        if ($iso8601) {\n            if ($time == 0) {\n                $date_string = \"0000-00-00T00:00:00Z\";\n            }\n            else {\n                $date_string =\n\"$yeart-$mon-${mday}T$hour:$min:${sec}Z\";\n            }\n        }\n        else {\n            if ($time == 0) {\n                $date_string = \"Xxx Xxx 00 0000 00:00:00\";\n            }\n            elsif ($month_num) {\n                $date_string =\n                  \"$digit_to_day{$wday} $mon $mday $yeart $hour:$min:$sec\";\n            }\n            else {\n                $date_string =\n\"$digit_to_day{$wday} $digit_to_month{$mon} $mday $yeart $hour:$min:$sec\";\n            }\n        }\n\n        #\n        # However, we only print the date if it's different from the one\n        # above.  We need to fill the empty space with blanks, though.\n        #\n        if ($old_date_string eq $date_string) {\n            if ($iso8601) {\n                $date_string = \"                    \";\n            }\n            else {\n                $date_string = \"                        \";\n            }\n            $prev_cnt++\n              if ($INDEX ne \"\");\n        }\n        else {\n            $old_date_string = $date_string;\n\n            # Indexing code\n            if ($INDEX ne \"\") {\n\n                # First time it is run\n                if ($prev_day eq \"\") {\n                    $prev_day  = \"$mday $wday $mon $yeart\";\n                    $prev_hour = $hour;\n                    $prev_time = $time;\n                    $prev_cnt  = 0;\n                }\n\n                # A new day, so print the results\n                elsif ($prev_day ne \"$mday $wday $mon $yeart\") {\n                    my @prev_vals = split(/ /, $prev_day);\n\n                    my $date_str;\n                    if ($month_num) {\n                        $date_str =\n                            \"$digit_to_day{$prev_vals[1]} \"\n                          . \"$prev_vals[2] \"\n                          . \"$prev_vals[0] ${prev_vals[3]}\";\n                    }\n                    else {\n                        $date_str =\n                            \"$digit_to_day{$prev_vals[1]} \"\n                          . \"$digit_to_month{$prev_vals[2]} \"\n                          . \"$prev_vals[0] ${prev_vals[3]}\";\n                    }\n\n                    $date_str .= \" $prev_hour:00:00\"\n                      if ($INDEX_TYPE == $INDEX_HOUR);\n\n                    print INDEX \"${date_str}${delim} $prev_cnt\\n\" if ($prev_time > 0);\n\n                    # Reset\n                    $prev_cnt  = 0;\n                    $prev_day  = \"$mday $wday $mon $yeart\";\n                    $prev_hour = $hour;\n                    $prev_time = $time;\n\n                }\n\n                # Same day, but new hour\n                elsif (($INDEX_TYPE == $INDEX_HOUR) && ($prev_hour != $hour)) {\n                    my @prev_vals = split(/ /, $prev_day);\n\n                    if ($month_num) {\n                        print INDEX \"$digit_to_day{$prev_vals[1]} \"\n                          . \"$prev_vals[2] \"\n                          . \"$prev_vals[0] ${prev_vals[3]} \"\n                          . \"$prev_hour:00:00${delim} $prev_cnt\\n\"\n                          if ($prev_time > 0);\n                    }\n                    else {\n                        print INDEX \"$digit_to_day{$prev_vals[1]} \"\n                          . \"$digit_to_month{$prev_vals[2]} \"\n                          . \"$prev_vals[0] ${prev_vals[3]} \"\n                          . \"$prev_hour:00:00${delim} $prev_cnt\\n\"\n                          if ($prev_time > 0);\n                    }\n\n                    # Reset\n                    $prev_cnt  = 0;\n                    $prev_hour = $hour;\n                    $prev_time = $time;\n                }\n                $prev_cnt++;\n            }\n        }\n\n        #\n        #  Muck around with the [mac]times string to make it pretty.\n        #\n        my $mactime_tmp = $timestr2macstr{$key};\n        my $mactime     = \"\";\n        if ($mactime_tmp =~ /m/) {\n            $mactime = \"m\";\n        }\n        else {\n            $mactime = \".\";\n        }\n\n        if ($mactime_tmp =~ /a/) {\n            $mactime .= \"a\";\n        }\n        else {\n            $mactime .= \".\";\n        }\n\n        if ($mactime_tmp =~ /c/) {\n            $mactime .= \"c\";\n        }\n        else {\n            $mactime .= \".\";\n        }\n\n        if ($mactime_tmp =~ /b/) {\n            $mactime .= \"b\";\n        }\n        else {\n            $mactime .= \".\";\n        }\n\n        my ($ls, $uids, $groups, $size) = split(/:/, $file2other{$file});\n\n        print \"FILE: $file MODES: $ls U: $uids G: $groups S: $size\\n\"\n          if $debug;\n\n        if ($COMMA == 0) {\n            printf(\"%s %8s %3s %s %-8s %-8s %-8s %s\\n\",\n                $date_string, $size, $mactime, $ls, $uids, $groups, $inode,\n                $file);\n        }\n        else {\n            # escape any quotes in filename\n            my $file_tmp = $file;\n            $file_tmp =~ s/\\\"/\\\"\\\"/g;\n            printf(\"%s,%s,%s,%s,%s,%s,%s,\\\"%s\\\"\\n\",\n                $old_date_string, $size, $mactime, $ls, $uids, $groups, $inode,\n                $file_tmp);\n        }\n    }\n\n    # Finish the index page for the last entry\n    if (($INDEX ne \"\") && ($prev_cnt > 0)) {\n        my @prev_vals = split(/ /, $prev_day);\n\n        my $date_str;\n        if ($month_num) {\n            $date_str =\n                \"$digit_to_day{$prev_vals[1]} \"\n              . \"$prev_vals[2] \"\n              . \"$prev_vals[0] ${prev_vals[3]}\";\n        }\n        else {\n            $date_str =\n                \"$digit_to_day{$prev_vals[1]} \"\n              . \"$digit_to_month{$prev_vals[2]} \"\n              . \"$prev_vals[0] ${prev_vals[3]}\";\n        }\n\n        $date_str .= \" $prev_hour:00:00\"\n          if ($INDEX_TYPE == $INDEX_HOUR);\n\n        print INDEX \"${date_str}${delim} $prev_cnt\\n\" if ($prev_time > 0);\n        close INDEX;\n    }\n}\n\n#\n#   Routines for reading and caching user and group information.  These\n# are used in multiple programs... it caches the info once, then hopefully\n# won't be used again.\n#\n#  Steve Romig, May 1991.\n#\n# Provides a bunch of routines and a bunch of arrays.  Routines\n# (and their usage):\n#\n#    load_passwd_info($use_getent, $file_name)\n#\n#\tloads user information into the %uname* and %uid* arrays\n#\t(see below).\n#\n#\tIf $use_getent is non-zero:\n#\t    get the info via repeated 'getpwent' calls.  This can be\n#\t    *slow* on some hosts, especially if they are running as a\n#\t    YP (NIS) client.\n#\tIf $use_getent is 0:\n#\t    if $file_name is \"\", then get the info from reading the\n#\t    results of \"ypcat passwd\" and from /etc/passwd.  Otherwise,\n#\t    read the named file.  The file should be in passwd(5)\n#\t    format.\n#\n#    load_group_info($use_gentent, $file_name)\n#\n#\tis similar to load_passwd_info.\n#\n# Information is stored in several convenient associative arrays:\n#\n#   %uid2names\t\tAssoc array, indexed by uid, value is list of\n#\t\t\tuser names with that uid, in form \"name name\n#\t\t\tname...\".\n#\n#   %gid2members\tAssoc array, indexed by gid, value is list of\n#\t\t\tgroup members in form \"name name name...\"\n#\n#   %gname2gid\t\tAssoc array, indexed by group name, value is\n#\t\t\tmatching gid.\n#\n#   %gid2names\t\tAssoc array, indexed by gid, value is the\n#\t\t\tlist of group names with that gid in form\n#\t\t\t\"name name name...\".\n#\n# You can also use routines named the same as the arrays - pass the index\n# as the arg, get back the value.  If you use this, get{gr|pw}{uid|gid|nam}\n# will be used to lookup entries that aren't found in the cache.\n#\n# To be done:\n#    probably ought to add routines to deal with full names.\n#    maybe there ought to be some anal-retentive checking of password\n#\tand group entries.\n#    probably ought to cache get{pw|gr}{nam|uid|gid} lookups also.\n#    probably ought to avoid overwriting existing entries (eg, duplicate\n#       names in password file would collide in the tables that are\n#\tindexed by name).\n#\n# Disclaimer:\n#    If you use YP and you use netgroup entries such as\n#\t+@servers::::::\n#\t+:*:::::/usr/local/utils/messages\n#    then loading the password file in with &load_passwd_info(0) will get\n#    you mostly correct YP stuff *except* that it won't do the password and\n#    shell substitutions as you'd expect.  You might want to use\n#    &load_passwd_info(1) instead to use getpwent calls to do the lookups,\n#    which would be more correct.\n#\n#\n#  minor changes to make it fit with the TCT program, 9/25/99, - dan\n# A whole lot removed to clean it up for TSK - July 2008 - Brian\n#\n\npackage main;\n\nmy $passwd_loaded = 0;    # flags to use to avoid reloading everything\nmy $group_loaded  = 0;    # unnecessarily...\n\n#\n# Update user information for the user named $name.  We cache the password,\n# uid, login group, home directory and shell.\n#\n\nsub add_pw_info {\n    my ($name, $tmp, $uid) = @_;\n\n    if ((defined $name) && ($name ne \"\")) {\n\n        if ((defined $uid) && ($uid ne \"\")) {\n            if (defined($uid2names{$uid})) {\n                $uid2names{$uid} .= \" $name\";\n            }\n            else {\n                $uid2names{$uid} = $name;\n            }\n        }\n    }\n}\n\n#\n# Update group information for the group named $name.  We cache the gid\n# and the list of group members.\n#\n\nsub add_gr_info {\n    my ($name, $tmp, $gid) = @_;\n\n    if ((defined $name) && ($name ne \"\")) {\n\n        if ((defined $gid) && ($gid ne \"\")) {\n            if (defined($gid2names{$gid})) {\n                $gid2names{$gid} .= \" $name\";\n            }\n            else {\n                $gid2names{$gid} = $name;\n            }\n        }\n    }\n}\n\nsub load_passwd_info {\n    my ($file_name) = @_;\n    my (@pw_info);\n\n    if ($passwd_loaded) {\n        return;\n    }\n\n    $passwd_loaded = 1;\n\n    open(FILE, $file_name)\n      || die \"can't open $file_name\";\n\n    while (<FILE>) {\n        chop;\n\n        if ($_ !~ /^\\+/) {\n            &add_pw_info(split(/:/));\n        }\n    }\n    close(FILE);\n}\n\nsub load_group_info {\n    my ($file_name) = @_;\n    my (@gr_info);\n\n    if ($group_loaded) {\n        return;\n    }\n\n    $group_loaded = 1;\n\n    open(FILE, $file_name)\n      || die \"can't open $file_name\";\n\n    while (<FILE>) {\n        chop;\n        if ($_ !~ /^\\+/) {\n            &add_gr_info(split(/:/));\n        }\n    }\n    close(FILE);\n}\n\n#\n# Split a time machine record.\n#\nsub tm_split {\n    my ($line) = @_;\n    my (@fields);\n\n    for (@fields = split(/\\|/, $line)) {\n        s/%([A-F0-9][A-F0-9])/pack(\"C\", hex($1))/egis;\n    }\n    return @fields;\n}\n1;\n\n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/licenses/IBM-LICENSE",
    "content": "IBM PUBLIC LICENSE VERSION 1.0 - CORONER TOOLKIT UTILITIES\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS IBM PUBLIC\nLICENSE (\"AGREEMENT\").  ANY USE, REPRODUCTION OR DISTRIBUTION OF THE\nPROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1.  DEFINITIONS\n\n\"Contribution\" means:  \n    a) in the case of International Business Machines Corporation (\"IBM\"), \n       the Original Program, and \n    b) in the case of each Contributor, \n       i)  changes to the Program, and\n       ii) additions to the Program;\n           where such changes and/or additions to the Program originate\n           from and are distributed by that particular Contributor.  \n           A Contribution 'originates' from a Contributor if it was added \n           to the Program by such Contributor itself or anyone acting on \n           such Contributor's behalf.  \n    Contributions do not include additions to the Program which:\n       (i)  are separate modules of software distributed in conjunction \n            with the Program under their own license agreement, and \n       (ii) are not derivative works of the Program.\n\n\"Contributor\" means IBM and any other entity that distributes the Program.\n\n\"Licensed Patents \" mean patent claims licensable by a Contributor which\nare necessarily infringed by the use or sale of its Contribution alone\nor when combined with the Program.\n\n\"Original Program\" means the original version of the software accompanying\nthis Agreement as released by IBM, including source code, object code\nand documentation, if any.\n\n\"Program\" means the Original Program and Contributions.\n\n\"Recipient\" means anyone who receives the Program under this Agreement, \nincluding all Contributors.\n\n2.  GRANT OF RIGHTS\n\n    a) Subject to the terms of this Agreement, each Contributor hereby\n    grants Recipient a non-exclusive, worldwide, royalty-free copyright\n    license to reproduce, prepare derivative works of, publicly display,\n    publicly perform, distribute and sublicense the Contribution of such\n    Contributor, if any, and such derivative works, in source code and\n    object code form.\n\n    b) Subject to the terms of this Agreement, each Contributor hereby\n    grants Recipient a non-exclusive, worldwide, royalty-free patent\n    license under Licensed Patents to make, use, sell, offer to sell,\n    import and otherwise transfer the Contribution of such Contributor,\n    if any, in source code and object code form.  This patent license\n    shall apply to the combination of the Contribution and the Program\n    if, at the time the Contribution is added by the Contributor, such\n    addition of the Contribution causes such combination to be covered\n    by the Licensed Patents.  The patent license shall not apply to any\n    other combinations which include the Contribution.  No hardware per\n    se is licensed hereunder.\n\n    c) Recipient understands that although each Contributor grants the\n    licenses to its Contributions set forth herein, no assurances are\n    provided by any Contributor that the Program does not infringe the\n    patent or other intellectual property rights of any other entity.\n    Each Contributor disclaims any liability to Recipient for claims\n    brought by any other entity based on infringement of intellectual\n    property rights or otherwise.  As a condition to exercising the rights\n    and licenses granted hereunder, each Recipient hereby assumes sole\n    responsibility to secure any other intellectual property rights\n    needed, if any.  For example, if a third party patent license\n    is required to allow Recipient to distribute the Program, it is\n    Recipient's responsibility to acquire that license before distributing\n    the Program.\n\n    d) Each Contributor represents that to its knowledge it has sufficient\n    copyright rights in its Contribution, if any, to grant the copyright\n    license set forth in this Agreement.\n\n3.  REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form \nunder its own license agreement, provided that:\n    a) it complies with the terms and conditions of this Agreement; and\n    b) its license agreement:\n       i)   effectively disclaims on behalf of all Contributors all\n            warranties and conditions, express and implied, including\n            warranties or conditions of title and non-infringement, and\n            implied warranties or conditions of merchantability and fitness\n            for a particular purpose;\n       ii)  effectively excludes on behalf of all Contributors all \n            liability for damages, including direct, indirect, special, \n            incidental and consequential damages, such as lost profits; \n       iii) states that any provisions which differ from this Agreement \n            are offered by that Contributor alone and not by any other \n            party; and\n       iv)  states that source code for the Program is available from \n            such Contributor, and informs licensees how to obtain it in a \n            reasonable manner on or through a medium customarily used for \n            software exchange. \n\nWhen the Program is made available in source code form:\n    a) it must be made available under this Agreement; and \n    b) a copy of this Agreement must be included with each copy of the \n       Program.  \n\nEach Contributor must include the following in a conspicuous location \nin the Program: \n\n    Copyright (c) 1997,1998,1999, International Business Machines\n    Corporation and others. All Rights Reserved.\n\nIn addition, each Contributor must identify itself as the originator of\nits Contribution, if any, in a manner that reasonably allows subsequent\nRecipients to identify the originator of the Contribution. \n\n4.  COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities\nwith respect to end users, business partners and the like.  While this\nlicense is intended to facilitate the commercial use of the Program, the\nContributor who includes the Program in a commercial product offering\nshould do so in a manner which does not create potential liability for\nother Contributors.   Therefore, if a Contributor includes the Program in\na commercial product offering, such Contributor (\"Commercial Contributor\")\nhereby agrees to defend and indemnify every other Contributor\n(\"Indemnified Contributor\") against any losses, damages and costs\n(collectively \"Losses\") arising from claims, lawsuits and other legal\nactions brought by a third party against the Indemnified Contributor to\nthe extent caused by the acts or omissions of such Commercial Contributor\nin connection with its distribution of the Program in a commercial\nproduct offering.  The obligations in this section do not apply to any\nclaims or Losses relating to any actual or alleged intellectual property\ninfringement.  In order to qualify, an Indemnified Contributor must:\n    a) promptly notify the Commercial Contributor in writing of such claim,\nand \n    b) allow the Commercial Contributor to control, and cooperate with\n       the Commercial Contributor in, the defense and any related \n       settlement negotiations.  The Indemnified Contributor may \n       participate in any such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial\nproduct offering, Product X.  That Contributor is then a Commercial\nContributor.  If that Commercial Contributor then makes performance\nclaims, or offers warranties related to Product X, those performance\nclaims and warranties are such Commercial Contributor's responsibility\nalone.  Under this section, the Commercial Contributor would have to\ndefend claims against the other Contributors related to those performance\nclaims and warranties, and if a court requires any other Contributor to\npay any damages as a result, the Commercial Contributor must pay those\ndamages.\n\n5.  NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED\nON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER\nEXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR\nCONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A\nPARTICULAR PURPOSE. Each Recipient is solely responsible for determining\nthe appropriateness of using and distributing the Program and assumes\nall risks associated with its exercise of rights under this Agreement,\nincluding but not limited to the risks and costs of program errors,\ncompliance with applicable laws, damage to or loss of data, programs or\nequipment, and unavailability or interruption of operations. \n\n6.  DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR\nANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING\nWITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION\nOF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF\nADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n7.  GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of\nthe remainder of the terms of this Agreement, and without further action\nby the parties hereto, such provision shall be reformed to the minimum\nextent necessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against a Contributor with\nrespect to a patent applicable to software (including a cross-claim or\ncounterclaim in a lawsuit), then any patent licenses granted by that\nContributor to such Recipient under this Agreement shall terminate\nas of the date such litigation is filed.  In addition, If Recipient\ninstitutes patent litigation against any entity (including a cross-claim\nor counterclaim in a lawsuit) alleging that the Program itself (excluding\ncombinations of the Program with other software or hardware) infringes\nsuch Recipient's patent(s), then such Recipient's rights granted under\nSection 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails\nto comply with any of the material terms or conditions of this Agreement\nand does not cure such failure in a reasonable period of time after\nbecoming aware of such noncompliance.  If all Recipient's rights under\nthis Agreement terminate, Recipient agrees to cease use and distribution\nof the Program as soon as reasonably practicable.  However, Recipient's\nobligations under this Agreement and any licenses granted by Recipient\nrelating to the Program shall continue and survive. \n\nIBM may publish new versions (including revisions) of this Agreement\nfrom time to time.  Each new version of the Agreement will be given a\ndistinguishing version number.  The Program (including Contributions)\nmay always be distributed subject to the version of the Agreement under\nwhich it was received. In addition, after a new version of the Agreement\nis published, Contributor may elect to distribute the Program (including\nits Contributions) under the new version. No one other than IBM has the\nright to modify this Agreement.  Except as expressly stated in Sections\n2(a) and 2(b) above, Recipient receives no rights or licenses to the\nintellectual property of any Contributor under this Agreement, whether\nexpressly, by implication, estoppel or otherwise.  All rights in the\nProgram not expressly granted under this Agreement are reserved.\n\nThis Agreement is governed by the laws of the State of New York and the\nintellectual property laws of the United States of America. No party to\nthis Agreement will bring a legal action under this Agreement more than\none year after the cause of action arose.  Each party waives its rights\nto a jury trial in any resulting litigation. \n"
  },
  {
    "path": "tools/sleuthkit-4.12.1-win32/licenses/cpl1.0.txt",
    "content": "Common Public License Version 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM\nCONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\n    a) in the case of the initial Contributor, the initial code and\ndocumentation distributed under this Agreement, and\n\n    b) in the case of each subsequent Contributor:\n\n    i) changes to the Program, and\n\n    ii) additions to the Program;\n\n    where such changes and/or additions to the Program originate from and are\ndistributed by that particular Contributor. A Contribution 'originates' from a\nContributor if it was added to the Program by such Contributor itself or anyone\nacting on such Contributor's behalf. Contributions do not include additions to\nthe Program which: (i) are separate modules of software distributed in\nconjunction with the Program under their own license agreement, and (ii) are not\nderivative works of the Program.\n\n\"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents \" mean patent claims licensable by a Contributor which are\nnecessarily infringed by the use or sale of its Contribution alone or when\ncombined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this Agreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement,\nincluding all Contributors.\n\n2. GRANT OF RIGHTS\n\n    a) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free copyright license to\nreproduce, prepare derivative works of, publicly display, publicly perform,\ndistribute and sublicense the Contribution of such Contributor, if any, and such\nderivative works, in source code and object code form.\n\n    b) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free patent license under Licensed\nPatents to make, use, sell, offer to sell, import and otherwise transfer the\nContribution of such Contributor, if any, in source code and object code form.\nThis patent license shall apply to the combination of the Contribution and the\nProgram if, at the time the Contribution is added by the Contributor, such\naddition of the Contribution causes such combination to be covered by the\nLicensed Patents. The patent license shall not apply to any other combinations\nwhich include the Contribution. No hardware per se is licensed hereunder.\n\n    c) Recipient understands that although each Contributor grants the licenses\nto its Contributions set forth herein, no assurances are provided by any\nContributor that the Program does not infringe the patent or other intellectual\nproperty rights of any other entity. Each Contributor disclaims any liability to\nRecipient for claims brought by any other entity based on infringement of\nintellectual property rights or otherwise. As a condition to exercising the\nrights and licenses granted hereunder, each Recipient hereby assumes sole\nresponsibility to secure any other intellectual property rights needed, if any.\nFor example, if a third party patent license is required to allow Recipient to\ndistribute the Program, it is Recipient's responsibility to acquire that license\nbefore distributing the Program.\n\n    d) Each Contributor represents that to its knowledge it has sufficient\ncopyright rights in its Contribution, if any, to grant the copyright license set\nforth in this Agreement.\n\n3. REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form under its\nown license agreement, provided that:\n\n    a) it complies with the terms and conditions of this Agreement; and\n\n    b) its license agreement:\n\n    i) effectively disclaims on behalf of all Contributors all warranties and\nconditions, express and implied, including warranties or conditions of title and\nnon-infringement, and implied warranties or conditions of merchantability and\nfitness for a particular purpose;\n\n    ii) effectively excludes on behalf of all Contributors all liability for\ndamages, including direct, indirect, special, incidental and consequential\ndamages, such as lost profits;\n\n    iii) states that any provisions which differ from this Agreement are offered\nby that Contributor alone and not by any other party; and\n\n    iv) states that source code for the Program is available from such\nContributor, and informs licensees how to obtain it in a reasonable manner on or\nthrough a medium customarily used for software exchange. \n\nWhen the Program is made available in source code form:\n\n    a) it must be made available under this Agreement; and\n\n    b) a copy of this Agreement must be included with each copy of the Program. \n\nContributors may not remove or alter any copyright notices contained within the\nProgram.\n\nEach Contributor must identify itself as the originator of its Contribution, if\nany, in a manner that reasonably allows subsequent Recipients to identify the\noriginator of the Contribution.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities with\nrespect to end users, business partners and the like. While this license is\nintended to facilitate the commercial use of the Program, the Contributor who\nincludes the Program in a commercial product offering should do so in a manner\nwhich does not create potential liability for other Contributors. Therefore, if\na Contributor includes the Program in a commercial product offering, such\nContributor (\"Commercial Contributor\") hereby agrees to defend and indemnify\nevery other Contributor (\"Indemnified Contributor\") against any losses, damages\nand costs (collectively \"Losses\") arising from claims, lawsuits and other legal\nactions brought by a third party against the Indemnified Contributor to the\nextent caused by the acts or omissions of such Commercial Contributor in\nconnection with its distribution of the Program in a commercial product\noffering. The obligations in this section do not apply to any claims or Losses\nrelating to any actual or alleged intellectual property infringement. In order\nto qualify, an Indemnified Contributor must: a) promptly notify the Commercial\nContributor in writing of such claim, and b) allow the Commercial Contributor to\ncontrol, and cooperate with the Commercial Contributor in, the defense and any\nrelated settlement negotiations. The Indemnified Contributor may participate in\nany such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial product\noffering, Product X. That Contributor is then a Commercial Contributor. If that\nCommercial Contributor then makes performance claims, or offers warranties\nrelated to Product X, those performance claims and warranties are such\nCommercial Contributor's responsibility alone. Under this section, the\nCommercial Contributor would have to defend claims against the other\nContributors related to those performance claims and warranties, and if a court\nrequires any other Contributor to pay any damages as a result, the Commercial\nContributor must pay those damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\nIMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each\nRecipient is solely responsible for determining the appropriateness of using and\ndistributing the Program and assumes all risks associated with its exercise of\nrights under this Agreement, including but not limited to the risks and costs of\nprogram errors, compliance with applicable laws, damage to or loss of data,\nprograms or equipment, and unavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY\nCONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\nPROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\nSTRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\nOUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS\nGRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under applicable\nlaw, it shall not affect the validity or enforceability of the remainder of the\nterms of this Agreement, and without further action by the parties hereto, such\nprovision shall be reformed to the minimum extent necessary to make such\nprovision valid and enforceable.\n\nIf Recipient institutes patent litigation against a Contributor with respect to\na patent applicable to software (including a cross-claim or counterclaim in a\nlawsuit), then any patent licenses granted by that Contributor to such Recipient\nunder this Agreement shall terminate as of the date such litigation is filed. In\naddition, if Recipient institutes patent litigation against any entity\n(including a cross-claim or counterclaim in a lawsuit) alleging that the Program\nitself (excluding combinations of the Program with other software or hardware)\ninfringes such Recipient's patent(s), then such Recipient's rights granted under\nSection 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to\ncomply with any of the material terms or conditions of this Agreement and does\nnot cure such failure in a reasonable period of time after becoming aware of\nsuch noncompliance. If all Recipient's rights under this Agreement terminate,\nRecipient agrees to cease use and distribution of the Program as soon as\nreasonably practicable. However, Recipient's obligations under this Agreement\nand any licenses granted by Recipient relating to the Program shall continue and\nsurvive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but in\norder to avoid inconsistency the Agreement is copyrighted and may only be\nmodified in the following manner. The Agreement Steward reserves the right to\npublish new versions (including revisions) of this Agreement from time to time.\nNo one other than the Agreement Steward has the right to modify this Agreement.\nIBM is the initial Agreement Steward. IBM may assign the responsibility to serve\nas the Agreement Steward to a suitable separate entity. Each new version of the\nAgreement will be given a distinguishing version number. The Program (including\nContributions) may always be distributed subject to the version of the Agreement\nunder which it was received. In addition, after a new version of the Agreement\nis published, Contributor may elect to distribute the Program (including its\nContributions) under the new version. Except as expressly stated in Sections\n2(a) and 2(b) above, Recipient receives no rights or licenses to the\nintellectual property of any Contributor under this Agreement, whether\nexpressly, by implication, estoppel or otherwise. All rights in the Program not\nexpressly granted under this Agreement are reserved.\n\nThis Agreement is governed by the laws of the State of New York and the\nintellectual property laws of the United States of America. No party to this\nAgreement will bring a legal action under this Agreement more than one year\nafter the cause of action arose. Each party waives its rights to a jury trial in\nany resulting litigation.\n"
  }
]