Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7ef151c4 | ||
|
|
c48e4343dc | ||
|
|
d31e837b62 | ||
|
|
f338ccb071 | ||
|
|
1cf9ec4a7c | ||
|
|
d474de1317 | ||
|
|
4c5d966057 | ||
|
|
dc58c0f3ca | ||
|
|
7f586830ba | ||
|
|
7bb0f87808 | ||
|
|
be58885e4f | ||
|
|
3e4a449c41 | ||
|
|
08533f3ee2 | ||
|
|
d58587cc65 | ||
|
|
72266489da | ||
|
|
dc72be5a17 | ||
|
|
812a493cf1 | ||
|
|
7f61d5db07 | ||
|
|
7a301e3a72 | ||
|
|
b6c73e46f2 | ||
|
|
05144a2dda |
@@ -50,6 +50,9 @@ Houses the logic for generic CLI context and active competition commands:
|
|||||||
|
|
||||||
### D. Forensics ([forensics.py](file:///home/venus/code/ctf/src/ctf/forensics.py))
|
### D. Forensics ([forensics.py](file:///home/venus/code/ctf/src/ctf/forensics.py))
|
||||||
Implements specialized forensic inspection utilities registered as a nested subgroup under the CLI:
|
Implements specialized forensic inspection utilities registered as a nested subgroup under the CLI:
|
||||||
|
* **Custom Extractor**: Implements our own pure-Python, zero-dependency parser for standard TIFF/EXIF tags, Adobe XMP XML blocks, Photoshop IPTC IIM records, and JPEG/PNG physical parameters, rather than importing large external libraries.
|
||||||
|
> [!NOTE]
|
||||||
|
> We designed and implemented our own custom EXIF scraper/parser from scratch in pure Python rather than importing an external library (like `exifread` or `Pillow`) to maintain zero dependencies and keep the toolchain highly lightweight and customizable.
|
||||||
* `info`: Inspects target file sizes, reads magic bytes, and warns if extensions do not match detected signatures.
|
* `info`: Inspects target file sizes, reads magic bytes, and warns if extensions do not match detected signatures.
|
||||||
* `flag-search`: Extracts printable string sequences (equivalent to GNU `strings`) and matches them against regular expression patterns to find potential flags.
|
* `flag-search`: Extracts printable string sequences (equivalent to GNU `strings`) and matches them against regular expression patterns to find potential flags.
|
||||||
|
|
||||||
@@ -74,6 +77,13 @@ Implements specialized forensic inspection utilities registered as a nested subg
|
|||||||
### A. Forensics Pipeline & Extended Metadata
|
### A. Forensics Pipeline & Extended Metadata
|
||||||
* **Pipeline Coordination**: A central runner that coordinates file inspections, signature verification, and string searches sequentially on target files.
|
* **Pipeline Coordination**: A central runner that coordinates file inspections, signature verification, and string searches sequentially on target files.
|
||||||
* **Carving & Extraction Toolkit**: Deep inspection and carving utilities (such as archive extracting, custom chunk/payload carvers, and EXIF/metadata header decoders).
|
* **Carving & Extraction Toolkit**: Deep inspection and carving utilities (such as archive extracting, custom chunk/payload carvers, and EXIF/metadata header decoders).
|
||||||
|
* **Chepy Machine Learning**: Plan to integrate the `chepy_ml` plugin later to perform automated heuristics, entropy analysis, and machine-learning-based format detection on unknown metadata payloads.
|
||||||
|
* **Targeted Forensic Metadata Parsing**: Implement decoders/parsers for high-signal forensics structures:
|
||||||
|
* *PNG*: `tEXt`, `zTXt`, `iTXt` metadata chunks.
|
||||||
|
* *PDF*: `/Info` dictionaries and `/Metadata` XML streams.
|
||||||
|
* *GIF*: Comment extensions and application loop extensions.
|
||||||
|
* *ELF/PE*: Executable header offsets, architectures, and section headers.
|
||||||
|
* *MP3*: ID3v1 and ID3v2 tag blocks (Title, Artist, custom Comments).
|
||||||
|
|
||||||
### B. Core Solving Infrastructure & QOL
|
### B. Core Solving Infrastructure & QOL
|
||||||
* **Solving Assistants**: Standard templates and modular helper scripts for repeating CTF patterns (e.g., base conversions, cipher decoding, and request generation).
|
* **Solving Assistants**: Standard templates and modular helper scripts for repeating CTF patterns (e.g., base conversions, cipher decoding, and request generation).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The primary goal is to use the context of CTF challenges (forensics, crypto, web
|
|||||||
4. **Educational Context:** Use the existing scripts and tools in this repository (like `psk_crack.py`, `exploit.sh`, or the `tools/` directory) as examples when explaining technical concepts.
|
4. **Educational Context:** Use the existing scripts and tools in this repository (like `psk_crack.py`, `exploit.sh`, or the `tools/` directory) as examples when explaining technical concepts.
|
||||||
5. **Vim Folding Markers:** Wrap all classes, command groups, subcommands, and functions inside project implementation files in standard Vim/Neovim folding syntax markers (`# {{{ <name>` and `# }}}`).
|
5. **Vim Folding Markers:** Wrap all classes, command groups, subcommands, and functions inside project implementation files in standard Vim/Neovim folding syntax markers (`# {{{ <name>` and `# }}}`).
|
||||||
6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_<filename>.py`.
|
6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_<filename>.py`.
|
||||||
7. **Write Only, Do Not Run**: The agent must only write code and tests. Do not attempt to run tests or execute command-line scripts; the user will handle all execution, verification, and testing tasks.
|
7. **Write Only, Do Not Run**: The agent must only write code and tests. Do not attempt to run tests, execute command-line scripts, or execute commands (like pytest) unless the user explicitly asks for execution in their prompt; the user will handle all execution, verification, and testing tasks.
|
||||||
|
|
||||||
## Interaction Workflow
|
## Interaction Workflow
|
||||||
When discussing new implementations, features, or additions:
|
When discussing new implementations, features, or additions:
|
||||||
@@ -20,7 +20,7 @@ When discussing new implementations, features, or additions:
|
|||||||
2. **The Pitch**: Provide a clear, technical pitch detailing the planned code modifications.
|
2. **The Pitch**: Provide a clear, technical pitch detailing the planned code modifications.
|
||||||
3. **Implementation Cycle**: Once the user approves the pitch:
|
3. **Implementation Cycle**: Once the user approves the pitch:
|
||||||
* **Add Test Cases**: Write or update the corresponding test cases in the test files (e.g. `tests/test_forensics.py`) following the SDET instructions.
|
* **Add Test Cases**: Write or update the corresponding test cases in the test files (e.g. `tests/test_forensics.py`) following the SDET instructions.
|
||||||
* **Git Commit Current State**: Create a git commit of the current workspace state (including the new tests) *before* modifying any implementation files.
|
* **Git Commit Current State**: Create a git commit of the current workspace state (including the new tests) *before* modifying any implementation files. The commit message MUST start with `[GEMINI]` as the author prefix (e.g., `git commit -m "[GEMINI] Write tests for forensics"`).
|
||||||
* **Update the Codebase**: Write/update the actual project implementation files as approved. Do **NOT** create a git commit after writing this implementation code. This ensures all implementation changes remain unstaged so the user can easily run `git diff` to review them.
|
* **Update the Codebase**: Write/update the actual project implementation files as approved. Do **NOT** create a git commit after writing this implementation code. This ensures all implementation changes remain unstaged so the user can easily run `git diff` to review them.
|
||||||
* **Hand Over**: Present the changes to the user so they can run the tests. Do not run the tests yourself.
|
* **Hand Over**: Present the changes to the user so they can run the tests. Do not run the tests yourself.
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,58 @@
|
|||||||
# CTF toolchain for linux
|
# AI-Enhanced CTF Toolchain & Learning Platform
|
||||||
This project is meant to be an ai-enhancing ctf toolchain to track your work and help you faster
|
|
||||||
The system keeps track of your file history, current ctf, challenge catagory, etc to make it easy to provide AI with the context to give you the right answer
|
|
||||||
The focus is on building a solid methodology to solve any problem.
|
|
||||||
|
|
||||||
|
A modern, lightweight Capture The Flag (CTF) utility and context manager written in Python. This tool standardizes solving methodologies, captures solver context, inspects file structures, and automatically intercepts flags in the console stream to facilitate agentic solving workflows.
|
||||||
|
|
||||||
# Dev flow
|
Managed using `uv` as the package manager and built with a modular, pure-Python architecture.
|
||||||
## initialize a competition
|
|
||||||
Take user input (eventually screenshots) and generate a new competition directory with directories for each catagory
|
|
||||||
## Initialize a challenge
|
|
||||||
Make a new challenge directory and cd into it
|
|
||||||
Review recently downloaded files, and add challenge files
|
|
||||||
## Progress tracker
|
|
||||||
Generate an active log file from command history
|
|
||||||
Accept user prompts to inform history
|
|
||||||
- finished (challenge finished)
|
|
||||||
- begin route (start a new path of exploration)
|
|
||||||
- help (prompts AI with challenge and progress to give reliable and relevant help)
|
|
||||||
- success (current path succeeded with previously run command)
|
|
||||||
- context (add context at this point in the log)
|
|
||||||
## Built in scripts and libraries
|
|
||||||
- built in rapid forensics testing and flag analysis based on context
|
|
||||||
## Write up aid
|
|
||||||
Shows all of your quickly commented notes
|
|
||||||
Creates prompt for AI to generate a write up showing your process
|
|
||||||
- feedback (generates feedback on your work flow and suggests improvements)
|
|
||||||
|
|
||||||
# Misc
|
---
|
||||||
the end goal is to create a methodology solid enough to build out a full AI tool chain around it while allowing tight human integration
|
|
||||||
|
|
||||||
|
## 🚀 Key Features
|
||||||
|
|
||||||
## features
|
### 1. Real-Time Flag Interception (Stdout Monitor)
|
||||||
- Cyberchef like decoding and magic feature
|
* **Automatic Flag Detection**: Standard output stream interceptor scans terminal output in real-time.
|
||||||
- regexing input text for flag
|
* **Persistent Capture**: Detected flags matching the active regular expression format are instantly saved to [config.toml](file:///home/venus/code/ctf/config.toml) (`last_flag`).
|
||||||
- flag selection and filtering with remembered prev. flags
|
|
||||||
# Tools
|
### 2. Custom Forensic Inspection
|
||||||
- forensics
|
Pure-Python, zero-dependency parsers extract metadata without using heavy external libraries:
|
||||||
- forensics tool for basic file analysis
|
* **POSIX Metadata**: Hard link counts, ownership, permission masks, and inode data.
|
||||||
|
* **Image Metadata**: Scrapes TIFF/EXIF tags, Adobe XMP XML blocks, and Photoshop IPTC IIM records.
|
||||||
|
* **Chunk/Comment Scrapers**: Parses PNG text chunks (`tEXt`, `zTXt`, `iTXt`) and extracts comments from JPEG and GIF comment blocks.
|
||||||
|
* **Physical Parameters**: Returns image parameters (width, height, color depth, etc.) for JPEG, PNG, and GIF.
|
||||||
|
|
||||||
|
### 3. Recursive Decoding Engine
|
||||||
|
* **Multi-layer Decoder**: Integrates with `chepy` to identify and decode nested strings.
|
||||||
|
* **Supported Formats**: Hex, Base64, Base32, Binary, URL percent-encoding, ROT13, and string reversal.
|
||||||
|
* **Intelligent Filtering**: Prevents spam by only outputting ROT13 and Reversal transformations if they yield valid flag formats.
|
||||||
|
|
||||||
|
### 4. Steganography Cracker
|
||||||
|
* **Steghide Wrapper**: Automatically brute-forces passphrase-protected files using standard dictionary wordlists.
|
||||||
|
* **Safe Execution**: Runs in isolated temporary sandboxes to keep the workspace clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ CLI Reference
|
||||||
|
|
||||||
|
### Context Commands
|
||||||
|
* `ctf set-competition <name>`: Configure active competition.
|
||||||
|
* `ctf set-flag-format`: Set expected flag regex format (supports original flag sample extraction).
|
||||||
|
* `ctf flag`: Output the last captured flag from the configuration.
|
||||||
|
|
||||||
|
### Forensics Commands
|
||||||
|
* `ctf inspect <file>`: Detailed metadata inspection.
|
||||||
|
* `-i, --inode`: Output POSIX file attributes only.
|
||||||
|
* `-e, --exif`: Output EXIF/XMP/IPTC data and decoded hints.
|
||||||
|
* `-p, --physical`: Output physical parameters of the target media.
|
||||||
|
* `ctf forensics flag-search <file>`: Extract printable characters (GNU `strings` style) and match them against regular expressions.
|
||||||
|
|
||||||
|
### Steganography Commands
|
||||||
|
* `ctf steg crack-steghide <file> -w <wordlist>`: Brute-force passphrase-protected files.
|
||||||
|
* `-o, --output <path>`: Write the extracted payload directly to a file on success.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Development & Testing
|
||||||
|
|
||||||
|
This project is built following strict **SDET workflows** to ensure pipeline correctness:
|
||||||
|
* **Sandbox Testing**: Unit tests run using a dedicated sandboxed environment under [tests/env](file:///home/venus/code/ctf/tests/env).
|
||||||
|
* **Testing Commands**: Use `uv run pytest` to execute the full integration test suite.
|
||||||
|
* **Dependency Management**: Dependencies are locked in [uv.lock](file:///home/venus/code/ctf/uv.lock) and managed in [pyproject.toml](file:///home/venus/code/ctf/pyproject.toml).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ competition = "picoctf"
|
|||||||
catagory = "textCat"
|
catagory = "textCat"
|
||||||
challenge = "testChal"
|
challenge = "testChal"
|
||||||
flag_format = "picoCTF\\{.*\\}"
|
flag_format = "picoCTF\\{.*\\}"
|
||||||
last_flag = "picoCTF{flag}"
|
last_flag = "flag{my_flag_here}"
|
||||||
|
|
||||||
[Enviroment]
|
[Enviroment]
|
||||||
ctf_dir = "/home/venus/ctf"
|
ctf_dir = "/home/venus/ctf"
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ This file tracks the completed progress and upcoming development milestones for
|
|||||||
|
|
||||||
### 📅 Phase 1: Functional Forensics Pipeline & Toolkit
|
### 📅 Phase 1: Functional Forensics Pipeline & Toolkit
|
||||||
* **Pipeline Automation**: Integrate the existing forensics command-line utilities into a cohesive analysis pipeline where files are automatically checked for magic bytes, file extensions, and flag patterns.
|
* **Pipeline Automation**: Integrate the existing forensics command-line utilities into a cohesive analysis pipeline where files are automatically checked for magic bytes, file extensions, and flag patterns.
|
||||||
* **Metadata & Extraction Toolkit**: Extend forensics tools to extract specific metadata (e.g., EXIF records, archive tables) and automate extraction/carving of nested data structures (e.g., binwalk-like carving, automated unzipping, extraction of hidden payloads).
|
* **Metadata & Extraction Toolkit**: Extend forensics tools to extract specific metadata and automate extraction/carving of nested data structures (e.g., binwalk-like carving, automated unzipping, extraction of hidden payloads).
|
||||||
|
* **Targeted Forensic Metadata Parsing**: Implement pure-Python parsers for high-signal forensics structures:
|
||||||
|
* *PNG Text Chunks*: Extract metadata from `tEXt`, `zTXt`, and `iTXt` blocks.
|
||||||
|
* *PDF Catalog Info*: Extract `/Info` and `/Metadata` XML properties.
|
||||||
|
* *GIF Application Extensions*: Extract comments and loop delays.
|
||||||
|
* *ELF/PE Executable Headers*: Read architecture, entry points, and section identifiers.
|
||||||
|
* *MP3 ID3 Tags*: Extract Title, Artist, Album, and standard Comment frames.
|
||||||
* **QOL Utilities**: Add standard format outputs (JSON, Rich logs) and automatic logging of analysis artifacts to speed up user-led inspections.
|
* **QOL Utilities**: Add standard format outputs (JSON, Rich logs) and automatic logging of analysis artifacts to speed up user-led inspections.
|
||||||
|
|
||||||
### 📅 Phase 2: Core CTF Solving & Solver QOL
|
### 📅 Phase 2: Core CTF Solving & Solver QOL
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"toml>=0.10.2",
|
"toml>=0.10.2",
|
||||||
"click",
|
"click",
|
||||||
"rich",
|
"rich",
|
||||||
|
"chepy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -13,17 +13,18 @@ def forensics_group():
|
|||||||
pass
|
pass
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# {{{ tf
|
|
||||||
@forensics_group.command()
|
|
||||||
def tf():
|
|
||||||
click.echo("hello from forensics")
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# {{{ inspect
|
# {{{ inspect
|
||||||
@forensics_group.command()
|
@forensics_group.command()
|
||||||
@click.argument('path', type=click.Path(exists=True))
|
@click.argument('path', type=click.Path(exists=True))
|
||||||
def inspect(path):
|
@click.option('-i', '--inode', is_flag=True, help="Print only POSIX inode metadata (without EXIF data).")
|
||||||
"""Lists all basic inode metadata about a file"""
|
@click.option('-e', '--exif', is_flag=True, help="Print only EXIF metadata (without POSIX inode metadata).")
|
||||||
|
@click.option('-p', '--physical', is_flag=True, help="Print only physical image parameters.")
|
||||||
|
def inspect(path, inode, exif, physical):
|
||||||
|
"""Lists all basic inode metadata and EXIF data about a file"""
|
||||||
|
flags = [inode, exif, physical]
|
||||||
|
if sum(flags) > 1:
|
||||||
|
raise click.UsageError("Cannot specify more than one of -i/--inode, -e/--exif, and -p/--physical.")
|
||||||
|
|
||||||
meta = get_metadata(Path(path))
|
meta = get_metadata(Path(path))
|
||||||
|
|
||||||
# Present using Rich Table
|
# Present using Rich Table
|
||||||
@@ -31,6 +32,8 @@ def inspect(path):
|
|||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
if not exif and not physical:
|
||||||
table = Table(title=f"Metadata: {meta.filename}", show_header=False)
|
table = Table(title=f"Metadata: {meta.filename}", show_header=False)
|
||||||
table.add_column("Key", style="bold cyan")
|
table.add_column("Key", style="bold cyan")
|
||||||
table.add_column("Value")
|
table.add_column("Value")
|
||||||
@@ -51,8 +54,69 @@ def inspect(path):
|
|||||||
if meta.extended_attributes:
|
if meta.extended_attributes:
|
||||||
xattr_str = ", ".join(f"{k}={v}" for k, v in meta.extended_attributes.items())
|
xattr_str = ", ".join(f"{k}={v}" for k, v in meta.extended_attributes.items())
|
||||||
table.add_row("Extended Attributes", xattr_str)
|
table.add_row("Extended Attributes", xattr_str)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
if physical:
|
||||||
|
if meta.physical_data:
|
||||||
|
table = Table(title="Physical Metadata", show_header=True)
|
||||||
|
table.add_column("Attribute", style="bold cyan")
|
||||||
|
table.add_column("Value")
|
||||||
|
for k, v in sorted(meta.physical_data.items()):
|
||||||
|
table.add_row(k, str(v))
|
||||||
|
console.print(table)
|
||||||
|
else:
|
||||||
|
console.print("[bold yellow]No physical metadata found.[/bold yellow]")
|
||||||
|
|
||||||
|
if not inode and not exif and not physical:
|
||||||
|
if meta.physical_data:
|
||||||
|
table = Table(title="Physical Metadata", show_header=True)
|
||||||
|
table.add_column("Attribute", style="bold cyan")
|
||||||
|
table.add_column("Value")
|
||||||
|
for k, v in sorted(meta.physical_data.items()):
|
||||||
|
table.add_row(k, str(v))
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
exif_displayed = False
|
||||||
|
if not inode and not physical:
|
||||||
|
if meta.exif_data:
|
||||||
|
exif_table = Table(title=f"EXIF Metadata: {meta.filename}", show_header=True)
|
||||||
|
exif_table.add_column("Tag", style="bold green")
|
||||||
|
exif_table.add_column("Value")
|
||||||
|
|
||||||
|
for tag, val in sorted(meta.exif_data.items()):
|
||||||
|
if isinstance(val, bytes):
|
||||||
|
val_str = val.hex().upper()
|
||||||
|
if len(val_str) > 60:
|
||||||
|
val_str = val_str[:57] + "..."
|
||||||
|
else:
|
||||||
|
val_str = str(val)
|
||||||
|
exif_table.add_row(tag, val_str)
|
||||||
|
|
||||||
|
console.print(exif_table)
|
||||||
|
exif_displayed = True
|
||||||
|
|
||||||
|
if meta.comment:
|
||||||
|
from rich.panel import Panel
|
||||||
|
console.print(Panel(
|
||||||
|
meta.comment,
|
||||||
|
title=f"[bold green]Comment: {meta.filename}[/bold green]",
|
||||||
|
border_style="green"
|
||||||
|
))
|
||||||
|
exif_displayed = True
|
||||||
|
|
||||||
|
if exif and not exif_displayed:
|
||||||
|
console.print("[bold yellow]No EXIF or comment metadata found.[/bold yellow]")
|
||||||
|
|
||||||
|
if meta.decoded_hints:
|
||||||
|
hints_table = Table(title="Decoded Metadata Hints (Hex/Base64)", show_header=True)
|
||||||
|
hints_table.add_column("Source Tag", style="bold yellow")
|
||||||
|
hints_table.add_column("Encoding", style="bold green")
|
||||||
|
hints_table.add_column("Decoded Value")
|
||||||
|
for src, dec_dict in sorted(meta.decoded_hints.items()):
|
||||||
|
for enc, dec_val in sorted(dec_dict.items()):
|
||||||
|
hints_table.add_row(src, enc, dec_val)
|
||||||
|
console.print(hints_table)
|
||||||
|
|
||||||
return meta
|
return meta
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# src/ctf/cli_steg.py
|
||||||
|
# {{{ imports
|
||||||
|
import click
|
||||||
|
from pathlib import Path
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from ctf.steg import crack_steghide
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ steg_group
|
||||||
|
@click.group(name="steg")
|
||||||
|
def steg_group():
|
||||||
|
""" Steganography tools and solvers """
|
||||||
|
pass
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ crack_steghide_cmd
|
||||||
|
@steg_group.command(name="crack-steghide")
|
||||||
|
@click.argument('path', type=click.Path(exists=True))
|
||||||
|
@click.option('-w', '--wordlist', type=click.Path(exists=True), required=True, help="Path to password wordlist.")
|
||||||
|
@click.option('-o', '--output', type=click.Path(), help="Path to write the extracted payload (if cracked).")
|
||||||
|
def crack_steghide_cmd(path, wordlist, output):
|
||||||
|
"""Attempt to crack steghide passphrases using a wordlist"""
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Read the wordlist
|
||||||
|
try:
|
||||||
|
with open(wordlist, "r", errors="ignore") as f:
|
||||||
|
words = [line.strip() for line in f if line.strip()]
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[bold red]Failed to read wordlist: {e}[/bold red]")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[*] Cracking {path} using {len(words)} passwords...")
|
||||||
|
result = crack_steghide(Path(path), words)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
console.print(Panel(
|
||||||
|
f"[bold green]✓ Successfully cracked![/bold green]\n"
|
||||||
|
f"[bold cyan]Password:[/bold cyan] {result.password}\n"
|
||||||
|
f"[bold cyan]Payload Size:[/bold cyan] {len(result.payload)} bytes",
|
||||||
|
title="Crack Result",
|
||||||
|
border_style="green"
|
||||||
|
))
|
||||||
|
|
||||||
|
# If output destination is supplied, write payload; otherwise print preview
|
||||||
|
if output:
|
||||||
|
try:
|
||||||
|
out_path = Path(output).resolve()
|
||||||
|
out_path.write_bytes(result.payload)
|
||||||
|
console.print(f"[green]✓ Extracted payload written to {out_path}[/green]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[bold red]Failed to write output file: {e}[/bold red]")
|
||||||
|
else:
|
||||||
|
# Show a safe, printable preview of the payload
|
||||||
|
try:
|
||||||
|
preview = result.payload.decode("utf-8")
|
||||||
|
# Truncate if long
|
||||||
|
if len(preview) > 300:
|
||||||
|
preview = preview[:300] + "\n..."
|
||||||
|
console.print(Panel(preview, title="Payload Preview", border_style="blue"))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Binary payload
|
||||||
|
hex_preview = result.payload.hex()[:100] + "..."
|
||||||
|
console.print(f"[yellow]Binary payload (hex preview): {hex_preview}[/yellow]")
|
||||||
|
else:
|
||||||
|
console.print(f"[bold red]✗ Cracking failed: {result.error_message}[/bold red]")
|
||||||
|
# }}}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# src/ctf/decoding.py
|
||||||
|
# {{{ imports
|
||||||
|
from chepy import Chepy
|
||||||
|
import re
|
||||||
|
from typing import Dict, Set
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ is_valid_flag
|
||||||
|
def is_valid_flag(s: str, original: str = "") -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the string contains a flag matching the active pattern or a generic pattern.
|
||||||
|
If original is provided, ensures that the decoded string contains a new unique flag
|
||||||
|
that was not already present in the original string.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ctf.utils import load_config
|
||||||
|
config = load_config()
|
||||||
|
flag_format = config.get("Competition", {}).get("flag_format", "")
|
||||||
|
except Exception:
|
||||||
|
flag_format = ""
|
||||||
|
|
||||||
|
patterns = []
|
||||||
|
if flag_format:
|
||||||
|
patterns.append(flag_format.strip("^$"))
|
||||||
|
patterns.append(r"(?i)[a-z0-9_-]+{[a-z0-9_!@#$%^&*()\-+=]+}")
|
||||||
|
|
||||||
|
decoded_flags = set()
|
||||||
|
for pattern in patterns:
|
||||||
|
for match in re.finditer(pattern, s):
|
||||||
|
decoded_flags.add(match.group(0))
|
||||||
|
|
||||||
|
if not decoded_flags:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if original:
|
||||||
|
original_flags = set()
|
||||||
|
for pattern in patterns:
|
||||||
|
for match in re.finditer(pattern, original):
|
||||||
|
original_flags.add(match.group(0))
|
||||||
|
# If there are no new flags in decoded_flags, return False
|
||||||
|
if not (decoded_flags - original_flags):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ attempt_decode
|
||||||
|
def attempt_decode(val: str, name: str, pattern: str, method_name: str, len_check=None, validator=None) -> str | None:
|
||||||
|
"""Helper to run regex matching, dynamic Chepy decoding, and string printability checks."""
|
||||||
|
# Find all matches sequentially and try decoding
|
||||||
|
for match in re.finditer(pattern, val):
|
||||||
|
matched_val = match.group(0)
|
||||||
|
if len_check and not len_check(matched_val):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
chepy_inst = Chepy(matched_val)
|
||||||
|
method = getattr(chepy_inst, method_name)
|
||||||
|
decoded = method().state
|
||||||
|
if isinstance(decoded, bytes):
|
||||||
|
dec_str = decoded.decode("utf-8")
|
||||||
|
elif isinstance(decoded, str):
|
||||||
|
dec_str = decoded
|
||||||
|
else:
|
||||||
|
dec_str = ""
|
||||||
|
|
||||||
|
if dec_str.strip() and all(32 <= ord(c) < 127 or c in "\r\n\t" for c in dec_str):
|
||||||
|
if validator and not validator(dec_str, val):
|
||||||
|
continue
|
||||||
|
return dec_str
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ try_decode_metadata
|
||||||
|
def try_decode_metadata(val: str, current_chain: str = "", max_depth: int = 10, seen: Set[str] = None) -> Dict[str, str]:
|
||||||
|
"""Attempts to decode a metadata value using defined formats recursively."""
|
||||||
|
if seen is None:
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
if not isinstance(val, str) or not val.strip() or max_depth <= 0:
|
||||||
|
return results
|
||||||
|
|
||||||
|
cleaned_val = val.strip()
|
||||||
|
if cleaned_val in seen:
|
||||||
|
return results
|
||||||
|
seen.add(cleaned_val)
|
||||||
|
|
||||||
|
# We define our decoders here using unanchored patterns. Note: for rot13 and reversed, we only allow them
|
||||||
|
# if they produce a valid flag to avoid spamming everyday metadata fields.
|
||||||
|
formats = [
|
||||||
|
("hex", r"[0-9a-fA-F]{4,}", "from_hex", lambda s: len(s) % 2 == 0, None),
|
||||||
|
("base64", r"[A-Za-z0-9+/=]{4,}", "from_base64", lambda s: len(s) % 4 == 0, None),
|
||||||
|
("base32", r"[A-Za-z2-7=]{4,}", "from_base32", lambda s: len(s) % 8 == 0, None),
|
||||||
|
("binary", r"[01]{8,}", "from_binary", lambda s: len(s) % 8 == 0, None),
|
||||||
|
("url", r"(?:%[0-9a-fA-F]{2})+", "from_url_encoding", None, None),
|
||||||
|
("rot13", r"[\x20-\x7E\s]{4,}", "rot_13", None, is_valid_flag),
|
||||||
|
("reversed", r"[\x20-\x7E\s]{4,}", "reverse", None, is_valid_flag),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, pattern, method, len_check, validator in formats:
|
||||||
|
decoded_val = attempt_decode(cleaned_val, name, pattern, method, len_check, validator)
|
||||||
|
if decoded_val is not None:
|
||||||
|
chain_name = f"{current_chain}->{name}" if current_chain else name
|
||||||
|
results[chain_name] = decoded_val
|
||||||
|
|
||||||
|
# Recurse with copy of seen to allow different branching paths to process same strings
|
||||||
|
nested_results = try_decode_metadata(
|
||||||
|
decoded_val,
|
||||||
|
current_chain=chain_name,
|
||||||
|
max_depth=max_depth - 1,
|
||||||
|
seen=set(seen)
|
||||||
|
)
|
||||||
|
results.update(nested_results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
# }}}
|
||||||
@@ -7,7 +7,9 @@ from pathlib import Path
|
|||||||
import stat
|
import stat
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict
|
import struct
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pwd
|
import pwd
|
||||||
@@ -16,6 +18,7 @@ except ImportError:
|
|||||||
pwd = None
|
pwd = None
|
||||||
grp = None
|
grp = None
|
||||||
|
|
||||||
|
# {{{ Common Signatures
|
||||||
COMMON_SIGNATURES = {
|
COMMON_SIGNATURES = {
|
||||||
b"\x89PNG\r\n\x1a\n": ("PNG Image", [".png"]),
|
b"\x89PNG\r\n\x1a\n": ("PNG Image", [".png"]),
|
||||||
b"\xff\xd8\xff": ("JPEG Image", [".jpg", ".jpeg"]),
|
b"\xff\xd8\xff": ("JPEG Image", [".jpg", ".jpeg"]),
|
||||||
@@ -33,6 +36,7 @@ COMMON_SIGNATURES = {
|
|||||||
b"BM": ("BMP Image", [".bmp"]),
|
b"BM": ("BMP Image", [".bmp"]),
|
||||||
b"ID3": ("MP3 Audio", [".mp3"]),
|
b"ID3": ("MP3 Audio", [".mp3"]),
|
||||||
}
|
}
|
||||||
|
# }}}
|
||||||
|
|
||||||
# {{{ FileMetadata
|
# {{{ FileMetadata
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -65,6 +69,18 @@ class FileMetadata:
|
|||||||
|
|
||||||
# Task 6: Extended Attributes
|
# Task 6: Extended Attributes
|
||||||
extended_attributes: Dict[str, str] = field(default_factory=dict)
|
extended_attributes: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# EXIF Data
|
||||||
|
exif_data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# Physical Image Data
|
||||||
|
physical_data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# Decoded metadata hints
|
||||||
|
decoded_hints: Dict[str, Dict[str, str]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# JPEG/PNG Comments
|
||||||
|
comment: str = ""
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# {{{ get_metadata
|
# {{{ get_metadata
|
||||||
@@ -146,6 +162,42 @@ def get_metadata(path: Path) -> FileMetadata:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
exif_data = get_exif(p)
|
||||||
|
comment = get_comment(p)
|
||||||
|
|
||||||
|
physical_data = {}
|
||||||
|
try:
|
||||||
|
with open(p, "rb") as f:
|
||||||
|
file_bytes = f.read()
|
||||||
|
if file_bytes.startswith(b"\xff\xd8\xff"):
|
||||||
|
physical_data = get_jpeg_physical(file_bytes)
|
||||||
|
elif file_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
physical_data = get_png_physical(file_bytes)
|
||||||
|
elif file_bytes.startswith(b"GIF87a") or file_bytes.startswith(b"GIF89a"):
|
||||||
|
physical_data = get_gif_physical(file_bytes)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
decoded_hints = {}
|
||||||
|
try:
|
||||||
|
from ctf.decoding import try_decode_metadata
|
||||||
|
if comment:
|
||||||
|
dec = try_decode_metadata(comment)
|
||||||
|
if dec:
|
||||||
|
decoded_hints["Comment"] = dec
|
||||||
|
for tag, val in exif_data.items():
|
||||||
|
if isinstance(val, str):
|
||||||
|
dec = try_decode_metadata(val)
|
||||||
|
if dec:
|
||||||
|
decoded_hints[f"EXIF:{tag}"] = dec
|
||||||
|
for attr, val in extended_attributes.items():
|
||||||
|
if isinstance(val, str):
|
||||||
|
dec = try_decode_metadata(val)
|
||||||
|
if dec:
|
||||||
|
decoded_hints[f"xattr:{attr}"] = dec
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return FileMetadata(
|
return FileMetadata(
|
||||||
filename=p.name,
|
filename=p.name,
|
||||||
size=size,
|
size=size,
|
||||||
@@ -162,7 +214,737 @@ def get_metadata(path: Path) -> FileMetadata:
|
|||||||
hard_links=hard_links,
|
hard_links=hard_links,
|
||||||
inode=inode,
|
inode=inode,
|
||||||
device=device,
|
device=device,
|
||||||
extended_attributes=extended_attributes
|
extended_attributes=extended_attributes,
|
||||||
|
exif_data=exif_data,
|
||||||
|
physical_data=physical_data,
|
||||||
|
decoded_hints=decoded_hints,
|
||||||
|
comment=comment
|
||||||
)
|
)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# {{{ EXIF tags
|
||||||
|
EXIF_TAGS = {
|
||||||
|
0x010e: "ImageDescription",
|
||||||
|
0x010f: "Make",
|
||||||
|
0x0110: "Model",
|
||||||
|
0x0112: "Orientation",
|
||||||
|
0x011a: "XResolution",
|
||||||
|
0x011b: "YResolution",
|
||||||
|
0x0128: "ResolutionUnit",
|
||||||
|
0x0131: "Software",
|
||||||
|
0x0132: "DateTime",
|
||||||
|
0x013b: "Artist",
|
||||||
|
0x8298: "Copyright",
|
||||||
|
0x829a: "ExposureTime",
|
||||||
|
0x829d: "FNumber",
|
||||||
|
0x8822: "ExposureProgram",
|
||||||
|
0x8825: "GPSInfo",
|
||||||
|
0x8827: "ISOSpeedRatings",
|
||||||
|
0x9000: "ExifVersion",
|
||||||
|
0x9003: "DateTimeOriginal",
|
||||||
|
0x9004: "DateTimeDigitized",
|
||||||
|
0x9201: "ShutterSpeedValue",
|
||||||
|
0x9202: "ApertureValue",
|
||||||
|
0x9204: "ExposureBiasValue",
|
||||||
|
0x9205: "MaxApertureValue",
|
||||||
|
0x9207: "MeteringMode",
|
||||||
|
0x9208: "LightSource",
|
||||||
|
0x9209: "Flash",
|
||||||
|
0x920a: "FocalLength",
|
||||||
|
0x9286: "UserComment",
|
||||||
|
0xa000: "FlashpixVersion",
|
||||||
|
0xa001: "ColorSpace",
|
||||||
|
0xa002: "ExifImageWidth",
|
||||||
|
0xa003: "ExifImageHeight",
|
||||||
|
0xa402: "ExposureMode",
|
||||||
|
0xa403: "WhiteBalance",
|
||||||
|
0xa405: "FocalLengthIn35mmFilm",
|
||||||
|
0xa406: "SceneCaptureType",
|
||||||
|
}# }}}
|
||||||
|
|
||||||
|
# {{{ parse_tiff
|
||||||
|
def parse_tiff(data: bytes) -> Dict[str, Any]:
|
||||||
|
"""Parses raw TIFF data block into EXIF tag dictionary."""
|
||||||
|
tags = {}
|
||||||
|
if len(data) < 8:
|
||||||
|
return tags
|
||||||
|
|
||||||
|
byte_order = data[0:2]
|
||||||
|
if byte_order == b"II":
|
||||||
|
endian = "<"
|
||||||
|
elif byte_order == b"MM":
|
||||||
|
endian = ">"
|
||||||
|
else:
|
||||||
|
return tags
|
||||||
|
|
||||||
|
magic = struct.unpack(f"{endian}H", data[2:4])[0]
|
||||||
|
if magic != 42:
|
||||||
|
return tags
|
||||||
|
|
||||||
|
ifd_offset = struct.unpack(f"{endian}I", data[4:8])[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
while ifd_offset != 0 and ifd_offset < len(data):
|
||||||
|
if ifd_offset + 2 > len(data):
|
||||||
|
break
|
||||||
|
num_entries = struct.unpack(f"{endian}H", data[ifd_offset:ifd_offset+2])[0]
|
||||||
|
entry_offset = ifd_offset + 2
|
||||||
|
|
||||||
|
for _ in range(num_entries):
|
||||||
|
if entry_offset + 12 > len(data):
|
||||||
|
break
|
||||||
|
tag, field_type, count, val_offset = struct.unpack(
|
||||||
|
f"{endian}HHII", data[entry_offset:entry_offset+12]
|
||||||
|
)
|
||||||
|
entry_offset += 12
|
||||||
|
|
||||||
|
# Resolve value based on type and count
|
||||||
|
val = None
|
||||||
|
type_sizes = {1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 7: 1, 9: 4, 10: 8}
|
||||||
|
size = type_sizes.get(field_type, 1) * count
|
||||||
|
|
||||||
|
if size <= 4:
|
||||||
|
raw_val = struct.pack(f"{endian}I", val_offset)[:size]
|
||||||
|
else:
|
||||||
|
if val_offset + size <= len(data):
|
||||||
|
raw_val = data[val_offset:val_offset+size]
|
||||||
|
else:
|
||||||
|
raw_val = b""
|
||||||
|
|
||||||
|
if field_type == 2: # ASCII
|
||||||
|
val = raw_val.split(b"\x00")[0].decode("utf-8", errors="ignore")
|
||||||
|
elif field_type == 3: # SHORT
|
||||||
|
fmt = f"{endian}" + "H" * count
|
||||||
|
if len(raw_val) >= 2 * count:
|
||||||
|
val = struct.unpack(fmt, raw_val[:2*count])
|
||||||
|
if count == 1:
|
||||||
|
val = val[0]
|
||||||
|
elif field_type == 4: # LONG
|
||||||
|
fmt = f"{endian}" + "I" * count
|
||||||
|
if len(raw_val) >= 4 * count:
|
||||||
|
val = struct.unpack(fmt, raw_val[:4*count])
|
||||||
|
if count == 1:
|
||||||
|
val = val[0]
|
||||||
|
elif field_type == 5: # RATIONAL
|
||||||
|
fmt = f"{endian}" + "II" * count
|
||||||
|
if len(raw_val) >= 8 * count:
|
||||||
|
unpacked = struct.unpack(fmt, raw_val[:8*count])
|
||||||
|
rationals = [f"{unpacked[i*2]}/{unpacked[i*2+1]}" for i in range(count)]
|
||||||
|
val = rationals[0] if count == 1 else rationals
|
||||||
|
elif field_type == 7: # UNDEFINED
|
||||||
|
val = raw_val
|
||||||
|
elif field_type == 9: # SLONG
|
||||||
|
fmt = f"{endian}" + "i" * count
|
||||||
|
if len(raw_val) >= 4 * count:
|
||||||
|
val = struct.unpack(fmt, raw_val[:4*count])
|
||||||
|
if count == 1:
|
||||||
|
val = val[0]
|
||||||
|
elif field_type == 10: # SRATIONAL
|
||||||
|
fmt = f"{endian}" + "ii" * count
|
||||||
|
if len(raw_val) >= 8 * count:
|
||||||
|
unpacked = struct.unpack(fmt, raw_val[:8*count])
|
||||||
|
rationals = [f"{unpacked[i*2]}/{unpacked[i*2+1]}" for i in range(count)]
|
||||||
|
val = rationals[0] if count == 1 else rationals
|
||||||
|
else:
|
||||||
|
val = raw_val
|
||||||
|
|
||||||
|
tag_name = EXIF_TAGS.get(tag, f"Tag_{hex(tag)}")
|
||||||
|
tags[tag_name] = val
|
||||||
|
|
||||||
|
if entry_offset + 4 > len(data):
|
||||||
|
break
|
||||||
|
ifd_offset = struct.unpack(f"{endian}I", data[entry_offset:entry_offset+4])[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return tags
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_jpeg_exif
|
||||||
|
def extract_jpeg_exif(data: bytes) -> bytes:
|
||||||
|
"""Extracts raw Exif/TIFF payload from JPEG APP1 segments."""
|
||||||
|
if not data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return b""
|
||||||
|
idx = 2
|
||||||
|
while idx < len(data) - 4:
|
||||||
|
if data[idx] == 0xff:
|
||||||
|
marker = data[idx+1]
|
||||||
|
if marker == 0xd9: # EOI
|
||||||
|
break
|
||||||
|
# Markers without length parameters
|
||||||
|
if marker in (0xd8, 0xd9, 0x00) or 0xd0 <= marker <= 0xd7:
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
length = struct.unpack(">H", data[idx+2:idx+4])[0]
|
||||||
|
if marker == 0xe1: # APP1
|
||||||
|
app1_data = data[idx+4:idx+4+length-2]
|
||||||
|
if app1_data.startswith(b"Exif\x00\x00"):
|
||||||
|
return app1_data[6:]
|
||||||
|
idx += 2 + length
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
return b""
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_png_exif
|
||||||
|
def extract_png_exif(data: bytes) -> bytes:
|
||||||
|
"""Extracts raw Exif/TIFF payload from PNG eXIf chunks."""
|
||||||
|
if not data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
return b""
|
||||||
|
idx = 8
|
||||||
|
while idx < len(data) - 8:
|
||||||
|
length = struct.unpack(">I", data[idx:idx+4])[0]
|
||||||
|
chunk_type = data[idx+4:idx+8]
|
||||||
|
if chunk_type == b"eXIf":
|
||||||
|
return data[idx+8:idx+8+length]
|
||||||
|
elif chunk_type == b"IEND":
|
||||||
|
break
|
||||||
|
idx += 12 + length
|
||||||
|
return b""
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ get_exif
|
||||||
|
def get_exif(path: Path) -> Dict[str, Any]:
|
||||||
|
"""Reads file, extracts Exif and XMP segments, and parses them to a tag dictionary."""
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
tags = {}
|
||||||
|
|
||||||
|
# 1. Parse standard EXIF
|
||||||
|
exif_data = b""
|
||||||
|
if data.startswith(b"\xff\xd8\xff"):
|
||||||
|
exif_data = extract_jpeg_exif(data)
|
||||||
|
elif data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
exif_data = extract_png_exif(data)
|
||||||
|
|
||||||
|
if exif_data:
|
||||||
|
try:
|
||||||
|
tags.update(parse_tiff(exif_data))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. Parse XMP (Adobe)
|
||||||
|
xmp_str = ""
|
||||||
|
if data.startswith(b"\xff\xd8\xff"):
|
||||||
|
xmp_str = extract_jpeg_xmp(data)
|
||||||
|
|
||||||
|
if xmp_str:
|
||||||
|
try:
|
||||||
|
tags.update(parse_xmp(xmp_str))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. Parse IPTC (Photoshop APP13) if JPEG
|
||||||
|
if data.startswith(b"\xff\xd8\xff"):
|
||||||
|
try:
|
||||||
|
tags.update(extract_jpeg_iptc(data))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. Parse PNG text chunks if PNG
|
||||||
|
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
try:
|
||||||
|
tags.update(parse_png_text_chunks(data))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return tags
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_jpeg_comment
|
||||||
|
def extract_jpeg_comment(data: bytes) -> str:
|
||||||
|
"""Extracts raw comment string from JPEG COM (0xfe) segments."""
|
||||||
|
if not data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return ""
|
||||||
|
idx = 2
|
||||||
|
while idx < len(data) - 4:
|
||||||
|
if data[idx] == 0xff:
|
||||||
|
marker = data[idx+1]
|
||||||
|
if marker == 0xd9: # EOI
|
||||||
|
break
|
||||||
|
if marker in (0xd8, 0xd9, 0x00) or 0xd0 <= marker <= 0xd7:
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
length = struct.unpack(">H", data[idx+2:idx+4])[0]
|
||||||
|
if marker == 0xfe: # COM
|
||||||
|
comment_data = data[idx+4:idx+4+length-2]
|
||||||
|
return comment_data.decode("utf-8", errors="ignore")
|
||||||
|
idx += 2 + length
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
return ""
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ get_comment
|
||||||
|
def get_comment(path: Path) -> str:
|
||||||
|
"""Reads file, checks headers, and extracts JPEG COM / GIF comments."""
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return extract_jpeg_comment(data)
|
||||||
|
elif data.startswith(b"GIF87a") or data.startswith(b"GIF89a"):
|
||||||
|
return extract_gif_comments(data)
|
||||||
|
return ""
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
#{{{ NS_map
|
||||||
|
NS_MAP = {
|
||||||
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
|
||||||
|
"http://creativecommons.org/ns#": "cc",
|
||||||
|
"http://purl.org/dc/elements/1.1/": "dc",
|
||||||
|
"http://ns.adobe.com/xap/1.0/": "xmp",
|
||||||
|
"http://ns.adobe.com/exif/1.0/": "exif",
|
||||||
|
"http://ns.adobe.com/tiff/1.0/": "tiff",
|
||||||
|
}
|
||||||
|
#}}}
|
||||||
|
|
||||||
|
# {{{ parse_xmp
|
||||||
|
def parse_xmp(xmp_str: str) -> Dict[str, Any]:
|
||||||
|
"""Parses Adobe XMP XML string and returns namespace-prefixed tags."""
|
||||||
|
metadata = {}
|
||||||
|
try:
|
||||||
|
start_idx = xmp_str.find("<x:xmpmeta")
|
||||||
|
if start_idx == -1:
|
||||||
|
return metadata
|
||||||
|
end_idx = xmp_str.find("</x:xmpmeta>")
|
||||||
|
if end_idx == -1:
|
||||||
|
return metadata
|
||||||
|
xml_data = xmp_str[start_idx:end_idx + len("</x:xmpmeta>")]
|
||||||
|
|
||||||
|
root = ET.fromstring(xml_data)
|
||||||
|
|
||||||
|
def get_clean_name(tag: str) -> str:
|
||||||
|
if "}" in tag:
|
||||||
|
ns, name = tag.split("}")
|
||||||
|
ns = ns.lstrip("{")
|
||||||
|
prefix = NS_MAP.get(ns, ns.split("/")[-1].split("#")[0])
|
||||||
|
return f"{prefix}:{name}"
|
||||||
|
return tag
|
||||||
|
|
||||||
|
for elem in root.iter():
|
||||||
|
clean_tag = get_clean_name(elem.tag)
|
||||||
|
if clean_tag == "rdf:Description":
|
||||||
|
for k, v in elem.attrib.items():
|
||||||
|
clean_k = get_clean_name(k)
|
||||||
|
if clean_k not in ("rdf:about", "rdf:Description"):
|
||||||
|
metadata[clean_k] = v
|
||||||
|
continue
|
||||||
|
if clean_tag in ("rdf:RDF", "x:xmpmeta"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
val = None
|
||||||
|
if elem.text and elem.text.strip():
|
||||||
|
val = elem.text.strip()
|
||||||
|
else:
|
||||||
|
attribs = {get_clean_name(k): v for k, v in elem.attrib.items()}
|
||||||
|
attribs.pop("rdf:about", None)
|
||||||
|
if "rdf:resource" in attribs:
|
||||||
|
val = attribs["rdf:resource"]
|
||||||
|
elif attribs:
|
||||||
|
val = ", ".join(f"{k} | {v}" if v else k for k, v in sorted(attribs.items()))
|
||||||
|
|
||||||
|
if val:
|
||||||
|
metadata[clean_tag] = val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return metadata
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_jpeg_xmp
|
||||||
|
def extract_jpeg_xmp(data: bytes) -> str:
|
||||||
|
"""Extracts XMP XML string from JPEG APP1 segments."""
|
||||||
|
if not data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return ""
|
||||||
|
idx = 2
|
||||||
|
while idx < len(data) - 4:
|
||||||
|
if data[idx] == 0xff:
|
||||||
|
marker = data[idx+1]
|
||||||
|
if marker == 0xd9: # EOI
|
||||||
|
break
|
||||||
|
if marker in (0xd8, 0xd9, 0x00) or 0xd0 <= marker <= 0xd7:
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
length = struct.unpack(">H", data[idx+2:idx+4])[0]
|
||||||
|
if marker == 0xe1: # APP1
|
||||||
|
app1_data = data[idx+4:idx+4+length-2]
|
||||||
|
if app1_data.startswith(b"http://ns.adobe.com/xap/1.0/\x00"):
|
||||||
|
return app1_data[29:].decode("utf-8", errors="ignore")
|
||||||
|
idx += 2 + length
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
return ""
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ get_jpeg_physical
|
||||||
|
def get_jpeg_physical(data: bytes) -> Dict[str, Any]:
|
||||||
|
"""Parses JPEG APP0 and SOF segments for physical dimensions and metadata."""
|
||||||
|
physical = {}
|
||||||
|
if not data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return physical
|
||||||
|
|
||||||
|
idx = 2
|
||||||
|
width = None
|
||||||
|
height = None
|
||||||
|
encoding_process = None
|
||||||
|
jfif_version = None
|
||||||
|
|
||||||
|
while idx < len(data) - 4:
|
||||||
|
if data[idx] == 0xff:
|
||||||
|
marker = data[idx+1]
|
||||||
|
if marker == 0xd9: # EOI
|
||||||
|
break
|
||||||
|
if marker in (0xd8, 0xd9, 0x00) or 0xd0 <= marker <= 0xd7:
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
length = struct.unpack(">H", data[idx+2:idx+4])[0]
|
||||||
|
segment_data = data[idx+4:idx+4+length-2]
|
||||||
|
|
||||||
|
# APP0 (JFIF)
|
||||||
|
if marker == 0xe0 and segment_data.startswith(b"JFIF\x00"):
|
||||||
|
if len(segment_data) >= 7:
|
||||||
|
major = segment_data[5]
|
||||||
|
minor = segment_data[6]
|
||||||
|
jfif_version = f"{major}.{minor:02d}"
|
||||||
|
physical["JFIF Version"] = jfif_version
|
||||||
|
|
||||||
|
# SOF markers
|
||||||
|
elif marker in (0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf):
|
||||||
|
if len(segment_data) >= 5:
|
||||||
|
precision = segment_data[0]
|
||||||
|
height = struct.unpack(">H", segment_data[1:3])[0]
|
||||||
|
width = struct.unpack(">H", segment_data[3:5])[0]
|
||||||
|
|
||||||
|
SOF_MARKERS = {
|
||||||
|
0xc0: "Baseline DCT, Huffman coding",
|
||||||
|
0xc1: "Extended Sequential DCT, Huffman coding",
|
||||||
|
0xc2: "Progressive DCT, Huffman coding",
|
||||||
|
0xc3: "Lossless, Huffman coding",
|
||||||
|
0xc5: "Differential Sequential DCT, Huffman coding",
|
||||||
|
0xc6: "Differential Progressive DCT, Huffman coding",
|
||||||
|
0xc7: "Differential Lossless, Huffman coding",
|
||||||
|
0xc9: "Extended Sequential DCT, Arithmetic coding",
|
||||||
|
0xca: "Progressive DCT, Arithmetic coding",
|
||||||
|
0xcb: "Lossless, Arithmetic coding",
|
||||||
|
0xcd: "Differential Sequential DCT, Arithmetic coding",
|
||||||
|
0xce: "Differential Progressive DCT, Arithmetic coding",
|
||||||
|
0xcf: "Differential Lossless, Arithmetic coding",
|
||||||
|
}
|
||||||
|
encoding_process = SOF_MARKERS.get(marker, "Unknown")
|
||||||
|
|
||||||
|
idx += 2 + length
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
if width is not None and height is not None:
|
||||||
|
physical["Image Size"] = f"{width}x{height}"
|
||||||
|
mp = (width * height) / 1000000.0
|
||||||
|
physical["Megapixels"] = f"{mp:.1f}"
|
||||||
|
if encoding_process is not None:
|
||||||
|
physical["Encoding Process"] = encoding_process
|
||||||
|
|
||||||
|
return physical
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ get_png_physical
|
||||||
|
def get_png_physical(data: bytes) -> Dict[str, Any]:
|
||||||
|
"""Parses PNG IHDR and pHYs chunks for physical properties."""
|
||||||
|
physical = {}
|
||||||
|
if not data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
return physical
|
||||||
|
|
||||||
|
idx = 8
|
||||||
|
while idx < len(data) - 8:
|
||||||
|
length = struct.unpack(">I", data[idx:idx+4])[0]
|
||||||
|
chunk_type = data[idx+4:idx+8]
|
||||||
|
chunk_data = data[idx+8:idx+8+length]
|
||||||
|
|
||||||
|
if chunk_type == b"IHDR":
|
||||||
|
if len(chunk_data) >= 13:
|
||||||
|
width = struct.unpack(">I", chunk_data[0:4])[0]
|
||||||
|
height = struct.unpack(">I", chunk_data[4:8])[0]
|
||||||
|
bit_depth = chunk_data[8]
|
||||||
|
color_type = chunk_data[9]
|
||||||
|
compression = chunk_data[10]
|
||||||
|
filter_method = chunk_data[11]
|
||||||
|
interlace = chunk_data[12]
|
||||||
|
|
||||||
|
physical["Image Size"] = f"{width}x{height}"
|
||||||
|
mp = (width * height) / 1000000.0
|
||||||
|
physical["Megapixels"] = f"{mp:.1f}"
|
||||||
|
physical["Bit Depth"] = f"{bit_depth} bits/sample"
|
||||||
|
|
||||||
|
color_types = {
|
||||||
|
0: "Grayscale",
|
||||||
|
2: "Truecolor",
|
||||||
|
3: "Indexed-color",
|
||||||
|
4: "Grayscale with Alpha",
|
||||||
|
6: "Truecolor with Alpha"
|
||||||
|
}
|
||||||
|
physical["Color Type"] = color_types.get(color_type, f"Unknown ({color_type})")
|
||||||
|
|
||||||
|
if compression == 0:
|
||||||
|
physical["Encoding Process"] = "Deflate/Inflate"
|
||||||
|
else:
|
||||||
|
physical["Encoding Process"] = f"Unknown compression ({compression})"
|
||||||
|
|
||||||
|
interlace_methods = {
|
||||||
|
0: "Noninterlaced",
|
||||||
|
1: "Adam7 Interlace"
|
||||||
|
}
|
||||||
|
physical["Interlace Method"] = interlace_methods.get(interlace, f"Unknown ({interlace})")
|
||||||
|
elif chunk_type == b"pHYs":
|
||||||
|
if len(chunk_data) >= 9:
|
||||||
|
x_res = struct.unpack(">I", chunk_data[0:4])[0]
|
||||||
|
y_res = struct.unpack(">I", chunk_data[4:8])[0]
|
||||||
|
unit = chunk_data[8]
|
||||||
|
unit_str = " meters" if unit == 1 else " (unknown unit)"
|
||||||
|
physical["Pixels Per Unit X"] = f"{x_res}{unit_str}"
|
||||||
|
physical["Pixels Per Unit Y"] = f"{y_res}{unit_str}"
|
||||||
|
elif chunk_type == b"IEND":
|
||||||
|
break
|
||||||
|
idx += 12 + length
|
||||||
|
|
||||||
|
return physical
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ get_gif_physical
|
||||||
|
def get_gif_physical(data: bytes) -> Dict[str, Any]:
|
||||||
|
"""Parses GIF logical screen descriptor for physical size."""
|
||||||
|
physical = {}
|
||||||
|
if not (data.startswith(b"GIF87a") or data.startswith(b"GIF89a")):
|
||||||
|
return physical
|
||||||
|
if len(data) >= 10:
|
||||||
|
width = struct.unpack("<H", data[6:8])[0]
|
||||||
|
height = struct.unpack("<H", data[8:10])[0]
|
||||||
|
physical["Image Size"] = f"{width}x{height}"
|
||||||
|
mp = (width * height) / 1000000.0
|
||||||
|
physical["Megapixels"] = f"{mp:.1f}"
|
||||||
|
physical["Encoding Process"] = "LZW"
|
||||||
|
return physical
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_jpeg_iptc
|
||||||
|
def extract_jpeg_iptc(data: bytes) -> Dict[str, str]:
|
||||||
|
"""Extracts IPTC/NAA metadata (Record 2) from Photoshop APP13 segments."""
|
||||||
|
iptc_metadata = {}
|
||||||
|
if not data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return iptc_metadata
|
||||||
|
|
||||||
|
IPTC_TAGS = {
|
||||||
|
5: "ObjectName",
|
||||||
|
25: "Keywords",
|
||||||
|
40: "SpecialInstructions",
|
||||||
|
80: "By-line",
|
||||||
|
85: "By-lineTitle",
|
||||||
|
90: "City",
|
||||||
|
95: "Province-State",
|
||||||
|
101: "Country-PrimaryLocationName",
|
||||||
|
105: "Headline",
|
||||||
|
110: "Credit",
|
||||||
|
115: "Source",
|
||||||
|
116: "CopyrightNotice",
|
||||||
|
120: "Caption-Abstract",
|
||||||
|
122: "Writer-Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = 2
|
||||||
|
while idx < len(data) - 4:
|
||||||
|
if data[idx] == 0xff:
|
||||||
|
marker = data[idx+1]
|
||||||
|
if marker == 0xd9: # EOI
|
||||||
|
break
|
||||||
|
if marker in (0xd8, 0xd9, 0x00) or 0xd0 <= marker <= 0xd7:
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
length = struct.unpack(">H", data[idx+2:idx+4])[0]
|
||||||
|
if marker == 0xed: # APP13 Photoshop block
|
||||||
|
app13_data = data[idx+4:idx+4+length-2]
|
||||||
|
if app13_data.startswith(b"Photoshop 3.0\x00"):
|
||||||
|
offset = 14
|
||||||
|
while offset < len(app13_data) - 12:
|
||||||
|
if app13_data[offset:offset+4] == b"8BIM":
|
||||||
|
offset += 4
|
||||||
|
irb_id = app13_data[offset:offset+2]
|
||||||
|
offset += 2
|
||||||
|
name_len = app13_data[offset]
|
||||||
|
offset += 1
|
||||||
|
name = app13_data[offset:offset+name_len]
|
||||||
|
offset += name_len
|
||||||
|
if (name_len + 1) % 2 != 0:
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
if offset + 4 > len(app13_data):
|
||||||
|
break
|
||||||
|
size = struct.unpack(">I", app13_data[offset:offset+4])[0]
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
if offset + size > len(app13_data):
|
||||||
|
break
|
||||||
|
irb_data = app13_data[offset:offset+size]
|
||||||
|
offset += size
|
||||||
|
if size % 2 != 0:
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
if irb_id == b"\x04\x04":
|
||||||
|
iptc_offset = 0
|
||||||
|
while iptc_offset < len(irb_data) - 5:
|
||||||
|
if irb_data[iptc_offset] == 0x1c:
|
||||||
|
record = irb_data[iptc_offset+1]
|
||||||
|
dataset = irb_data[iptc_offset+2]
|
||||||
|
ds_size = struct.unpack(">H", irb_data[iptc_offset+3:iptc_offset+5])[0]
|
||||||
|
iptc_offset += 5
|
||||||
|
if iptc_offset + ds_size <= len(irb_data):
|
||||||
|
ds_data = irb_data[iptc_offset:iptc_offset+ds_size]
|
||||||
|
iptc_offset += ds_size
|
||||||
|
if record == 2:
|
||||||
|
tag_name = IPTC_TAGS.get(dataset, f"IPTC_2_{dataset}")
|
||||||
|
val = ds_data.decode("utf-8", errors="ignore")
|
||||||
|
iptc_metadata[tag_name] = val
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
iptc_offset += 1
|
||||||
|
idx += 2 + length
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return iptc_metadata
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ parse_png_text_chunks
|
||||||
|
def parse_png_text_chunks(data: bytes) -> Dict[str, str]:
|
||||||
|
"""Decompresses and extracts standard tEXt/zTXt/iTXt PNG text metadata."""
|
||||||
|
import zlib
|
||||||
|
text_metadata = {}
|
||||||
|
if not data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
return text_metadata
|
||||||
|
idx = 8
|
||||||
|
while idx < len(data) - 8:
|
||||||
|
length = struct.unpack(">I", data[idx:idx+4])[0]
|
||||||
|
chunk_type = data[idx+4:idx+8]
|
||||||
|
chunk_data = data[idx+8:idx+8+length]
|
||||||
|
if chunk_type == b"tEXt":
|
||||||
|
parts = chunk_data.split(b"\x00", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].decode("utf-8", errors="ignore")
|
||||||
|
val = parts[1].decode("utf-8", errors="ignore")
|
||||||
|
text_metadata[key] = val
|
||||||
|
elif chunk_type == b"zTXt":
|
||||||
|
parts = chunk_data.split(b"\x00", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].decode("utf-8", errors="ignore")
|
||||||
|
remaining = parts[1]
|
||||||
|
if len(remaining) > 1:
|
||||||
|
comp_method = remaining[0]
|
||||||
|
comp_text = remaining[1:]
|
||||||
|
if comp_method == 0:
|
||||||
|
try:
|
||||||
|
val = zlib.decompress(comp_text).decode("utf-8", errors="ignore")
|
||||||
|
text_metadata[key] = val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif chunk_type == b"iTXt":
|
||||||
|
parts = chunk_data.split(b"\x00", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].decode("utf-8", errors="ignore")
|
||||||
|
remaining = parts[1]
|
||||||
|
if len(remaining) >= 2:
|
||||||
|
comp_flag = remaining[0]
|
||||||
|
comp_method = remaining[1]
|
||||||
|
rem = remaining[2:]
|
||||||
|
parts2 = rem.split(b"\x00", 1)
|
||||||
|
if len(parts2) == 2:
|
||||||
|
lang_tag = parts2[0].decode("utf-8", errors="ignore")
|
||||||
|
rem = parts2[1]
|
||||||
|
parts3 = rem.split(b"\x00", 1)
|
||||||
|
if len(parts3) == 2:
|
||||||
|
trans_key = parts3[0].decode("utf-8", errors="ignore")
|
||||||
|
text_bytes = parts3[1]
|
||||||
|
if comp_flag == 1 and comp_method == 0:
|
||||||
|
try:
|
||||||
|
val = zlib.decompress(text_bytes).decode("utf-8", errors="ignore")
|
||||||
|
text_metadata[key] = val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
val = text_bytes.decode("utf-8", errors="ignore")
|
||||||
|
text_metadata[key] = val
|
||||||
|
elif chunk_type == b"IEND":
|
||||||
|
break
|
||||||
|
idx += 12 + length
|
||||||
|
return text_metadata
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ extract_gif_comments
|
||||||
|
def extract_gif_comments(data: bytes) -> str:
|
||||||
|
"""Extracts comments from GIF comment extensions (\x21\xfe) sequentially."""
|
||||||
|
if not (data.startswith(b"GIF87a") or data.startswith(b"GIF89a")):
|
||||||
|
return ""
|
||||||
|
comments = []
|
||||||
|
idx = 6
|
||||||
|
if idx + 7 > len(data):
|
||||||
|
return ""
|
||||||
|
packed_byte = data[idx+4]
|
||||||
|
global_color_table_present = bool(packed_byte & 0x80)
|
||||||
|
global_color_table_size = 2 ** ((packed_byte & 0x07) + 1)
|
||||||
|
idx += 7
|
||||||
|
if global_color_table_present:
|
||||||
|
idx += 3 * global_color_table_size
|
||||||
|
|
||||||
|
while idx < len(data) - 2:
|
||||||
|
intro = data[idx]
|
||||||
|
if intro == 0x21: # Extension Introducer
|
||||||
|
ext_label = data[idx+1]
|
||||||
|
idx += 2
|
||||||
|
if ext_label == 0xfe: # Comment Extension
|
||||||
|
comment_parts = []
|
||||||
|
while idx < len(data):
|
||||||
|
block_len = data[idx]
|
||||||
|
idx += 1
|
||||||
|
if block_len == 0:
|
||||||
|
break
|
||||||
|
if idx + block_len <= len(data):
|
||||||
|
comment_parts.append(data[idx:idx+block_len].decode("utf-8", errors="ignore"))
|
||||||
|
idx += block_len
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
comments.append("".join(comment_parts))
|
||||||
|
else:
|
||||||
|
while idx < len(data):
|
||||||
|
block_len = data[idx]
|
||||||
|
idx += 1
|
||||||
|
if block_len == 0:
|
||||||
|
break
|
||||||
|
idx += block_len
|
||||||
|
elif intro == 0x2c: # Image Descriptor
|
||||||
|
if idx + 10 > len(data):
|
||||||
|
break
|
||||||
|
packed = data[idx+9]
|
||||||
|
local_table = bool(packed & 0x80)
|
||||||
|
local_table_size = 2 ** ((packed & 0x07) + 1)
|
||||||
|
idx += 10
|
||||||
|
if local_table:
|
||||||
|
idx += 3 * local_table_size
|
||||||
|
idx += 1
|
||||||
|
while idx < len(data):
|
||||||
|
block_len = data[idx]
|
||||||
|
idx += 1
|
||||||
|
if block_len == 0:
|
||||||
|
break
|
||||||
|
idx += block_len
|
||||||
|
elif intro == 0x3b: # Trailer
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
return "\n".join(comments)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# Parses and calls commands
|
# Parses and calls commands
|
||||||
|
|
||||||
from ctf.commands import basic_group
|
from ctf.commands import basic_group
|
||||||
from ctf.cli_forensics import forensics_group
|
from ctf.cli_forensics import forensics_group, inspect
|
||||||
|
from ctf.cli_steg import steg_group
|
||||||
import click
|
import click
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
@@ -64,6 +65,7 @@ class FlagDetectorStream:
|
|||||||
@click.option("-p", "--plain", is_flag=True, help="Print raw flag without flavor text.")
|
@click.option("-p", "--plain", is_flag=True, help="Print raw flag without flavor text.")
|
||||||
def flag_cmd(plain):
|
def flag_cmd(plain):
|
||||||
"""Retrieve the last detected flag from config."""
|
"""Retrieve the last detected flag from config."""
|
||||||
|
# TODO add support for multiple flags and flag selection
|
||||||
global _active_detector
|
global _active_detector
|
||||||
if _active_detector is not None:
|
if _active_detector is not None:
|
||||||
_active_detector.detecting = False
|
_active_detector.detecting = False
|
||||||
@@ -76,7 +78,7 @@ def flag_cmd(plain):
|
|||||||
click.echo("No flag has been detected yet.")
|
click.echo("No flag has been detected yet.")
|
||||||
return
|
return
|
||||||
if plain:
|
if plain:
|
||||||
click.echo(last_flag, nl=False)
|
click.echo(last_flag)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Last detected flag: {last_flag}")
|
click.echo(f"Last detected flag: {last_flag}")
|
||||||
finally:
|
finally:
|
||||||
@@ -84,15 +86,19 @@ def flag_cmd(plain):
|
|||||||
_active_detector.detecting = True
|
_active_detector.detecting = True
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# {{{ cli
|
||||||
|
@click.group()
|
||||||
|
def cli(): pass
|
||||||
|
|
||||||
|
cli.add_command(forensics_group)
|
||||||
|
cli.add_command(basic_group)
|
||||||
|
cli.add_command(flag_cmd)
|
||||||
|
cli.add_command(inspect)
|
||||||
|
cli.add_command(steg_group)
|
||||||
|
# }}}
|
||||||
|
|
||||||
# {{{ main
|
# {{{ main
|
||||||
def main():
|
def main():
|
||||||
@click.group()
|
|
||||||
def cli(): pass
|
|
||||||
|
|
||||||
cli.add_command(forensics_group)
|
|
||||||
cli.add_command(basic_group)
|
|
||||||
cli.add_command(flag_cmd)
|
|
||||||
|
|
||||||
from ctf.utils import load_config
|
from ctf.utils import load_config
|
||||||
config = load_config("/home/venus/code/ctf/config.toml")
|
config = load_config("/home/venus/code/ctf/config.toml")
|
||||||
flag_format = config.get("Competition", {}).get("flag_format", "")
|
flag_format = config.get("Competition", {}).get("flag_format", "")
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# src/ctf/steg.py
|
||||||
|
# {{{ imports
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ SteghideCrackResult
|
||||||
|
@dataclass
|
||||||
|
class SteghideCrackResult:
|
||||||
|
success: bool
|
||||||
|
password: str | None = None
|
||||||
|
payload: bytes | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ crack_steghide
|
||||||
|
def crack_steghide(file_path: Path, wordlist: list[str]) -> SteghideCrackResult:
|
||||||
|
"""Tries to extract data from a file using steghide with a wordlist."""
|
||||||
|
if not shutil.which("steghide"):
|
||||||
|
return SteghideCrackResult(success=False, error_message="steghide executable not found on system PATH.")
|
||||||
|
|
||||||
|
file_path = Path(file_path).resolve()
|
||||||
|
if not file_path.exists():
|
||||||
|
return SteghideCrackResult(success=False, error_message=f"File not found: {file_path}")
|
||||||
|
|
||||||
|
for password in wordlist:
|
||||||
|
password = password.strip()
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
out_file = Path(tmpdir) / "extracted_payload"
|
||||||
|
cmd = ["steghide", "extract", "-sf", str(file_path), "-p", password, "-xf", str(out_file), "-f"]
|
||||||
|
try:
|
||||||
|
# Capture standard outputs; timeout to prevent indefinite hangs
|
||||||
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
|
||||||
|
if result.returncode == 0 and out_file.exists():
|
||||||
|
return SteghideCrackResult(
|
||||||
|
success=True,
|
||||||
|
password=password,
|
||||||
|
payload=out_file.read_bytes()
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
return SteghideCrackResult(success=False, error_message=str(e))
|
||||||
|
|
||||||
|
return SteghideCrackResult(success=False, error_message="Password not found in wordlist.")
|
||||||
|
# }}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
picoCTF{h1dd3n_1n_1m4g3_5d4cba73}
|
||||||
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,2 @@
|
|||||||
|
‰PNG
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8 B |
|
After Width: | Height: | Size: 24 B |
|
After Width: | Height: | Size: 28 B |
|
After Width: | Height: | Size: 34 B |
|
After Width: | Height: | Size: 32 B |
|
After Width: | Height: | Size: 69 B |
|
After Width: | Height: | Size: 53 B |
|
After Width: | Height: | Size: 41 B |
|
After Width: | Height: | Size: 85 B |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 436 B |
@@ -0,0 +1,2 @@
|
|||||||
|
‰PNG
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8 B |
@@ -0,0 +1,2 @@
|
|||||||
|
‰PNG
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8 B |
@@ -64,6 +64,10 @@ def test_basic_set_flag_format_original_option_cli():
|
|||||||
# We pass -o SKY-1111-000 and input "2" to choose the second suggested pattern
|
# We pass -o SKY-1111-000 and input "2" to choose the second suggested pattern
|
||||||
result = runner.invoke(basic_group, ["set-flag-format", "-o", "SKY-1111-000"], input="2\n")
|
result = runner.invoke(basic_group, ["set-flag-format", "-o", "SKY-1111-000"], input="2\n")
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
comp_name = original_config.get("Competition", {}).get("competition", "")
|
||||||
|
if comp_name:
|
||||||
|
assert f"Suggested regex patterns for competition {comp_name}:" in result.output
|
||||||
|
else:
|
||||||
assert "Suggested regex patterns:" in result.output
|
assert "Suggested regex patterns:" in result.output
|
||||||
assert "Select a pattern index" in result.output
|
assert "Select a pattern index" in result.output
|
||||||
assert "Flag format set to" in result.output
|
assert "Flag format set to" in result.output
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# tests/test_decoding.py
|
||||||
|
from ctf.decoding import try_decode_metadata
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_hex
|
||||||
|
def test_try_decode_metadata_hex():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes standard hex string payloads.
|
||||||
|
"""
|
||||||
|
# Hex encoding of "picoCTF{hex_flag_hint}"
|
||||||
|
hex_str = "7069636f4354467b6865785f666c61675f68696e747d"
|
||||||
|
res = try_decode_metadata(hex_str)
|
||||||
|
assert res.get("hex") == "picoCTF{hex_flag_hint}"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_base64
|
||||||
|
def test_try_decode_metadata_base64():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes standard base64 string payloads.
|
||||||
|
"""
|
||||||
|
# Base64 encoding of "flag{b64_metadata}"
|
||||||
|
b64_str = "ZmxhZ3tiNjRfbWV0YWRhdGF9"
|
||||||
|
res = try_decode_metadata(b64_str)
|
||||||
|
assert res.get("base64") == "flag{b64_metadata}"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_non_decodable
|
||||||
|
def test_try_decode_metadata_non_decodable():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata returns empty results for invalid or non-printable payloads.
|
||||||
|
"""
|
||||||
|
# Invalid characters
|
||||||
|
res_invalid = try_decode_metadata("invalid!characters")
|
||||||
|
assert not res_invalid
|
||||||
|
|
||||||
|
# Not even length for hex, not modulo 4 for base64
|
||||||
|
res_odd = try_decode_metadata("1")
|
||||||
|
assert not res_odd
|
||||||
|
|
||||||
|
# Non-printable decoded content (binary raw data)
|
||||||
|
# Hex: \x01\x02\x03\x04
|
||||||
|
res_binary = try_decode_metadata("01020304")
|
||||||
|
assert not res_binary
|
||||||
|
# {{{ test_try_decode_metadata_base32
|
||||||
|
def test_try_decode_metadata_base32():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes standard base32 string payloads.
|
||||||
|
"""
|
||||||
|
# Base32 encoding of "flag{b32_metadata}"
|
||||||
|
b32_str = "MZWGCZ33MIZTEX3NMV2GCZDBORQX2==="
|
||||||
|
res = try_decode_metadata(b32_str)
|
||||||
|
assert res.get("base32") == "flag{b32_metadata}"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_binary
|
||||||
|
def test_try_decode_metadata_binary():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes binary strings.
|
||||||
|
"""
|
||||||
|
# Binary encoding of "flag{bin}"
|
||||||
|
bin_str = "011001100110110001100001011001110111101101100010011010010110111001111101"
|
||||||
|
res = try_decode_metadata(bin_str)
|
||||||
|
assert res.get("binary") == "flag{bin}"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_url
|
||||||
|
def test_try_decode_metadata_url():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes percent/URL encoded payloads.
|
||||||
|
"""
|
||||||
|
url_str = "%66%6c%61%67%7b%75%72%6c%7d"
|
||||||
|
res = try_decode_metadata(url_str)
|
||||||
|
assert res.get("url") == "flag{url}"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_rot13_only_flags
|
||||||
|
def test_try_decode_metadata_rot13_only_flags():
|
||||||
|
"""
|
||||||
|
Verifies that rot13 decodes flags but ignores standard text noise.
|
||||||
|
"""
|
||||||
|
# ROT13 of "flag{rot13}" -> "synt{ebg13}"
|
||||||
|
rot_str = "synt{ebg13}"
|
||||||
|
res = try_decode_metadata(rot_str)
|
||||||
|
assert res.get("rot13") == "flag{rot13}"
|
||||||
|
|
||||||
|
# ROT13 of "normal text" -> "abezny grkg"
|
||||||
|
res_noise = try_decode_metadata("abezny grkg")
|
||||||
|
assert "rot13" not in res_noise
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_reversed_only_flags
|
||||||
|
def test_try_decode_metadata_reversed_only_flags():
|
||||||
|
"""
|
||||||
|
Verifies that reversed decodes flags but ignores standard text noise.
|
||||||
|
"""
|
||||||
|
# Reversed of "flag{rev}" -> "}ver{galf"
|
||||||
|
rev_str = "}ver{galf"
|
||||||
|
res = try_decode_metadata(rev_str)
|
||||||
|
assert res.get("reversed") == "flag{rev}"
|
||||||
|
|
||||||
|
# Reversed of "normal text" -> "txet lamron"
|
||||||
|
res_noise = try_decode_metadata("txet lamron")
|
||||||
|
assert "reversed" not in res_noise
|
||||||
|
# {{{ test_try_decode_metadata_nested
|
||||||
|
def test_try_decode_metadata_nested():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata recursively decodes nested encodings and reports the path.
|
||||||
|
"""
|
||||||
|
# Payload: base64(hex("flag{nested_flag}"))
|
||||||
|
nested_str = "NjY2YzYxNjc3YjZlNjU3Mzc0NjU2NDVmNjY2YzYxNjc3ZA=="
|
||||||
|
res = try_decode_metadata(nested_str)
|
||||||
|
assert res.get("base64") == "666c61677b6e65737465645f666c61677d"
|
||||||
|
assert res.get("base64->hex") == "flag{nested_flag}"
|
||||||
|
# {{{ test_try_decode_metadata_unanchored
|
||||||
|
def test_try_decode_metadata_unanchored():
|
||||||
|
"""
|
||||||
|
Verifies that try_decode_metadata successfully decodes unanchored substrings (e.g. "steghide:cEF6endvcmQ=").
|
||||||
|
"""
|
||||||
|
res = try_decode_metadata("steghide:cEF6endvcmQ=")
|
||||||
|
assert res.get("base64") == "pAzzword"
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# {{{ test_try_decode_metadata_rot13_unique_only
|
||||||
|
def test_try_decode_metadata_rot13_unique_only():
|
||||||
|
"""
|
||||||
|
Verifies that rot13 only decodes unique flags and ignores flags already visible.
|
||||||
|
"""
|
||||||
|
# Case 1: Flag is encoded and not present in plaintext in the original string.
|
||||||
|
res = try_decode_metadata("synt{ebg13_havdhr}")
|
||||||
|
assert res.get("rot13") == "flag{rot13_unique}"
|
||||||
|
|
||||||
|
# Case 2: Flag is already present in plaintext in the original string.
|
||||||
|
res_duplicate = try_decode_metadata("flag{rot13_unique} synt{ebg13_havdhr}")
|
||||||
|
assert "rot13" not in res_duplicate
|
||||||
|
# }}}
|
||||||
|
|
||||||
@@ -248,6 +248,37 @@ def test_forensics_inspect_cli_success():
|
|||||||
assert "Detected Type" in result.output
|
assert "Detected Type" in result.output
|
||||||
assert "PNG Image" in result.output
|
assert "PNG Image" in result.output
|
||||||
|
|
||||||
|
def test_root_inspect_cli_success():
|
||||||
|
"""
|
||||||
|
Verifies that 'ctf inspect' successfully runs at the root level and prints
|
||||||
|
the metadata in a formatted table.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "test_file_root.png"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"\x89PNG\r\n\x1a\n")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["inspect", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Metadata: test_file_root.png" in result.output
|
||||||
|
assert "PNG Image" in result.output
|
||||||
|
|
||||||
|
def test_nested_inspect_cli_success():
|
||||||
|
"""
|
||||||
|
Verifies that 'ctf forensics inspect' successfully runs via the root CLI.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "test_file_nested.png"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"\x89PNG\r\n\x1a\n")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["forensics", "inspect", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Metadata: test_file_nested.png" in result.output
|
||||||
|
assert "PNG Image" in result.output
|
||||||
|
|
||||||
def test_forensics_inspect_cli_missing_file():
|
def test_forensics_inspect_cli_missing_file():
|
||||||
"""
|
"""
|
||||||
Verifies that the CLI fails gracefully when a non-existent file path is specified.
|
Verifies that the CLI fails gracefully when a non-existent file path is specified.
|
||||||
@@ -271,4 +302,335 @@ def test_forensics_signatures_cli():
|
|||||||
|
|
||||||
# (Old flag-detect tests removed)
|
# (Old flag-detect tests removed)
|
||||||
|
|
||||||
|
def test_pure_exif_parsing():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses EXIF fields from a mock PNG.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
tiff_data = (
|
||||||
|
b"II\x2a\x00\x08\x00\x00\x00" # TIFF Header
|
||||||
|
b"\x02\x00" # Num entries
|
||||||
|
b"\x0f\x01\x02\x00\x05\x00\x00\x00\x26\x00\x00\x00" # Entry 1 (Make: offset 38)
|
||||||
|
b"\x10\x01\x02\x00\x06\x00\x00\x00\x2b\x00\x00\x00" # Entry 2 (Model: offset 43)
|
||||||
|
b"\x00\x00\x00\x00" # Next IFD offset
|
||||||
|
b"TEST\x00" # Make value
|
||||||
|
b"MODEL\x00" # Model value
|
||||||
|
)
|
||||||
|
png_header = b"\x89PNG\r\n\x1a\n"
|
||||||
|
exif_chunk_type = b"eXIf"
|
||||||
|
exif_chunk_len = struct.pack(">I", len(tiff_data))
|
||||||
|
exif_chunk_crc = b"\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
mock_png = png_header + exif_chunk_len + exif_chunk_type + tiff_data + exif_chunk_crc
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_exif_image.png"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_png)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.exif_data.get("Make") == "TEST"
|
||||||
|
assert meta.exif_data.get("Model") == "MODEL"
|
||||||
|
|
||||||
|
def test_exif_cli_options():
|
||||||
|
"""
|
||||||
|
Verifies CLI options -i/--inode and -e/--exif work as expected.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "mock_exif_image.png"
|
||||||
|
|
||||||
|
# Verify mutual exclusion error
|
||||||
|
result_err = runner.invoke(cli, ["inspect", "-i", "-e", str(test_file)])
|
||||||
|
assert result_err.exit_code != 0
|
||||||
|
assert "Cannot specify more than one" in result_err.output
|
||||||
|
|
||||||
|
# Verify --inode hides EXIF data table
|
||||||
|
result_inode = runner.invoke(cli, ["inspect", "-i", str(test_file)])
|
||||||
|
assert result_inode.exit_code == 0
|
||||||
|
assert "Metadata: mock_exif_image.png" in result_inode.output
|
||||||
|
assert "EXIF Metadata" not in result_inode.output
|
||||||
|
|
||||||
|
# Verify --exif hides POSIX table but displays EXIF data table
|
||||||
|
result_exif = runner.invoke(cli, ["inspect", "-e", str(test_file)])
|
||||||
|
assert result_exif.exit_code == 0
|
||||||
|
assert "Metadata: mock_exif_image.png" not in result_exif.output
|
||||||
|
assert "EXIF Metadata" in result_exif.output
|
||||||
|
assert "mock_exif_image" in result_exif.output
|
||||||
|
assert "Make" in result_exif.output
|
||||||
|
assert "TEST" in result_exif.output
|
||||||
|
|
||||||
|
def test_jpeg_comment_extraction():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses a JPEG comment (COM marker).
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
comment_text = b"picoCTF{test_comment_flag}"
|
||||||
|
comment_len = len(comment_text) + 2
|
||||||
|
mock_jpeg = (
|
||||||
|
b"\xff\xd8"
|
||||||
|
b"\xff\xfe"
|
||||||
|
+ struct.pack(">H", comment_len)
|
||||||
|
+ comment_text
|
||||||
|
+ b"\xff\xd9"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_comment.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.comment == "picoCTF{test_comment_flag}"
|
||||||
|
|
||||||
|
def test_exif_cli_options_warning():
|
||||||
|
"""
|
||||||
|
Verifies CLI warns when no EXIF or comment is found and -e option is used.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "clean_no_exif.png"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"\x89PNG\r\n\x1a\n")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["inspect", "-e", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "No EXIF or comment metadata found" in result.output
|
||||||
|
|
||||||
|
def test_exif_cli_comment_only():
|
||||||
|
"""
|
||||||
|
Verifies CLI displays comment when -e is used and only comment is found.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
import struct
|
||||||
|
runner = CliRunner()
|
||||||
|
comment_text = b"flag{comment_only}"
|
||||||
|
comment_len = len(comment_text) + 2
|
||||||
|
mock_jpeg = b"\xff\xd8\xff\xfe" + struct.pack(">H", comment_len) + comment_text
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "comment_only.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["inspect", "-e", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Comment" in result.output
|
||||||
|
assert "flag{comment_only}" in result.output
|
||||||
|
|
||||||
|
def test_adobe_xmp_extraction():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses XMP fields (like license resource).
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
xmp_xml = (
|
||||||
|
b"<x:xmpmeta xmlns:x='adobe:ns:meta/'>\n"
|
||||||
|
b"<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n"
|
||||||
|
b" <rdf:Description rdf:about='' xmlns:cc='http://creativecommons.org/ns#'>\n"
|
||||||
|
b" <cc:license rdf:resource='cGljb0NURnt0ZXN0X3htcF9mbGFnfQ=='/>\n"
|
||||||
|
b" </rdf:Description>\n"
|
||||||
|
b"</rdf:RDF>\n"
|
||||||
|
b"</x:xmpmeta>"
|
||||||
|
)
|
||||||
|
xmp_prefix = b"http://ns.adobe.com/xap/1.0/\x00"
|
||||||
|
app1_payload = xmp_prefix + xmp_xml
|
||||||
|
app1_len = len(app1_payload) + 2
|
||||||
|
mock_jpeg = (
|
||||||
|
b"\xff\xd8"
|
||||||
|
b"\xff\xe1"
|
||||||
|
+ struct.pack(">H", app1_len)
|
||||||
|
+ app1_payload
|
||||||
|
+ b"\xff\xd9"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_xmp.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.exif_data.get("cc:license") == "cGljb0NURnt0ZXN0X3htcF9mbGFnfQ=="
|
||||||
|
|
||||||
|
def test_adobe_xmp_cli():
|
||||||
|
"""
|
||||||
|
Verifies that the ctf inspect CLI prints the parsed XMP fields.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "mock_xmp.jpg"
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["inspect", "-e", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "EXIF Metadata" in result.output
|
||||||
|
assert "cc:license" in result.output
|
||||||
|
assert "cGljb0NURnt0ZXN0X3htcF9mbGFnfQ==" in result.output
|
||||||
|
|
||||||
|
def test_jpeg_physical_parsing():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses JPEG physical parameters (JFIF & SOF).
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
app0_payload = b"JFIF\x00\x01\x02\x01\x00\x48\x00\x48\x00\x00"
|
||||||
|
app0_block = b"\xff\xe0" + struct.pack(">H", len(app0_payload) + 2) + app0_payload
|
||||||
|
|
||||||
|
sof_payload = b"\x08\x03\xe8\x05\xdc\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01"
|
||||||
|
sof_block = b"\xff\xc0" + struct.pack(">H", len(sof_payload) + 2) + sof_payload
|
||||||
|
|
||||||
|
mock_jpeg = b"\xff\xd8" + app0_block + sof_block + b"\xff\xd9"
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_physical.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.physical_data.get("JFIF Version") == "1.02"
|
||||||
|
assert meta.physical_data.get("Image Size") == "1500x1000"
|
||||||
|
assert meta.physical_data.get("Megapixels") == "1.5"
|
||||||
|
assert meta.physical_data.get("Encoding Process") == "Baseline DCT, Huffman coding"
|
||||||
|
|
||||||
|
def test_jpeg_iptc_parsing():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses JPEG IPTC metadata from APP13.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
iptc_ds = b"\x1c\x02\x74\x00\x0ePicoCTF Rights"
|
||||||
|
|
||||||
|
irb_id = b"\x04\x04"
|
||||||
|
irb_name = b"\x00\x00"
|
||||||
|
irb_size = struct.pack(">I", len(iptc_ds))
|
||||||
|
irb_block = b"8BIM" + irb_id + irb_name + irb_size + iptc_ds
|
||||||
|
|
||||||
|
app13_payload = b"Photoshop 3.0\x00" + irb_block
|
||||||
|
app13_block = b"\xff\xed" + struct.pack(">H", len(app13_payload) + 2) + app13_payload
|
||||||
|
|
||||||
|
mock_jpeg = b"\xff\xd8" + app13_block + b"\xff\xd9"
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_iptc.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.exif_data.get("CopyrightNotice") == "PicoCTF Rights"
|
||||||
|
|
||||||
|
def test_cli_physical_option():
|
||||||
|
"""
|
||||||
|
Verifies the ctf inspect CLI supports -p/--physical and mutual exclusion rules.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
test_file = TEST_ENV / "mock_physical.jpg"
|
||||||
|
|
||||||
|
# Mutual exclusion check
|
||||||
|
result_err = runner.invoke(cli, ["inspect", "-p", "-e", str(test_file)])
|
||||||
|
assert result_err.exit_code != 0
|
||||||
|
assert "Cannot specify more than one" in result_err.output
|
||||||
|
|
||||||
|
# Physical view check
|
||||||
|
result_phys = runner.invoke(cli, ["inspect", "-p", str(test_file)])
|
||||||
|
assert result_phys.exit_code == 0
|
||||||
|
assert "Physical Metadata" in result_phys.output
|
||||||
|
assert "Image Size" in result_phys.output
|
||||||
|
assert "1500x1000" in result_phys.output
|
||||||
|
assert "Metadata: mock_physical.jpg" not in result_phys.output
|
||||||
|
assert "EXIF Metadata" not in result_phys.output
|
||||||
|
def test_png_text_chunks_decompression():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses tEXt and compressed zTXt chunks.
|
||||||
|
"""
|
||||||
|
import zlib
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# 1. tEXt chunk: Keyword (Copyright) + NUL + Text (PicoCTF)
|
||||||
|
text_data = b"Copyright\x00PicoCTF"
|
||||||
|
text_chunk = struct.pack(">I", len(text_data)) + b"tEXt" + text_data + b"\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
# 2. zTXt chunk: Keyword (Author) + NUL + CompMethod(0) + Deflated Text (John Doe)
|
||||||
|
deflated = zlib.compress(b"John Doe")
|
||||||
|
ztxt_data = b"Author\x00\x00" + deflated
|
||||||
|
ztxt_chunk = struct.pack(">I", len(ztxt_data)) + b"zTXt" + ztxt_data + b"\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
png_data = b"\x89PNG\r\n\x1a\n" + text_chunk + ztxt_chunk + b"\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_text_chunks.png"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(png_data)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.exif_data.get("Copyright") == "PicoCTF"
|
||||||
|
assert meta.exif_data.get("Author") == "John Doe"
|
||||||
|
|
||||||
|
def test_gif_comment_parsing():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata successfully parses GIF comment blocks.
|
||||||
|
"""
|
||||||
|
gif_data = (
|
||||||
|
b"GIF89a"
|
||||||
|
b"\x01\x00\x01\x00\x00\x00\x00"
|
||||||
|
b"\x21\xfe\x0aGIFComment\x00"
|
||||||
|
b"\x3b"
|
||||||
|
)
|
||||||
|
test_file = TEST_ENV / "mock_comment.gif"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(gif_data)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.comment == "GIFComment"
|
||||||
|
|
||||||
|
def test_adobe_xmp_formatting():
|
||||||
|
"""
|
||||||
|
Verifies that get_metadata parses rdf:Description attributes and formats other
|
||||||
|
attributes with ' | ' instead of '='.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
xmp_xml = (
|
||||||
|
b"<x:xmpmeta xmlns:x='adobe:ns:meta/'>\n"
|
||||||
|
b"<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n"
|
||||||
|
b" <rdf:Description rdf:about='' xmlns:xmp='http://ns.adobe.com/xap/1.0/' xmlns:exif='http://ns.adobe.com/exif/1.0/'>\n"
|
||||||
|
b" <xmp:CreatorTool>Photoshop</xmp:CreatorTool>\n"
|
||||||
|
b" <exif:Flash exif:Fired='True' exif:Mode='1'/>\n"
|
||||||
|
b" <exif:EmptyAttr exif:Value=''/>\n"
|
||||||
|
b" </rdf:Description>\n"
|
||||||
|
b"</rdf:RDF>\n"
|
||||||
|
b"</x:xmpmeta>"
|
||||||
|
)
|
||||||
|
xmp_prefix = b"http://ns.adobe.com/xap/1.0/\x00"
|
||||||
|
app1_payload = xmp_prefix + xmp_xml
|
||||||
|
app1_len = len(app1_payload) + 2
|
||||||
|
mock_jpeg = (
|
||||||
|
b"\xff\xd8"
|
||||||
|
b"\xff\xe1"
|
||||||
|
+ struct.pack(">H", app1_len)
|
||||||
|
+ app1_payload
|
||||||
|
+ b"\xff\xd9"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_xmp_formatting.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
meta = get_metadata(test_file)
|
||||||
|
assert meta.exif_data.get("xmp:CreatorTool") == "Photoshop"
|
||||||
|
assert meta.exif_data.get("exif:Flash") == "exif:Fired | True, exif:Mode | 1"
|
||||||
|
assert meta.exif_data.get("exif:EmptyAttr") == "exif:Value"
|
||||||
|
assert "rdf:Description" not in meta.exif_data
|
||||||
|
|
||||||
|
def test_inspect_decoded_hints_cli():
|
||||||
|
"""
|
||||||
|
Verifies that inspect command successfully decodes and prints hex/base64 encoded metadata hints.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# Create a mock JPEG with a base64 encoded comment: "ZmxhZ3tiNjRfbWV0YWRhdGF9" -> "flag{b64_metadata}"
|
||||||
|
import struct
|
||||||
|
comment_text = b"ZmxhZ3tiNjRfbWV0YWRhdGF9"
|
||||||
|
comment_len = len(comment_text) + 2
|
||||||
|
mock_jpeg = b"\xff\xd8\xff\xfe" + struct.pack(">H", comment_len) + comment_text + b"\xff\xd9"
|
||||||
|
|
||||||
|
test_file = TEST_ENV / "mock_encoded_comment.jpg"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(mock_jpeg)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["inspect", str(test_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Decoded Metadata Hints" in result.output
|
||||||
|
assert "Comment" in result.output
|
||||||
|
assert "base64" in result.output
|
||||||
|
assert "flag{b64_metadata}" in result.output
|
||||||
|
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ def test_flag_cmd_cli():
|
|||||||
assert result_std.exit_code == 0
|
assert result_std.exit_code == 0
|
||||||
assert "Last detected flag: flag{test_cli_flag}" in result_std.output
|
assert "Last detected flag: flag{test_cli_flag}" in result_std.output
|
||||||
|
|
||||||
# Test plain flag command output (no flavor text, no newline)
|
# Test plain flag command output (no flavor text, has newline)
|
||||||
result_plain = runner.invoke(cli, ["flag", "--plain"])
|
result_plain = runner.invoke(cli, ["flag", "--plain"])
|
||||||
assert result_plain.exit_code == 0
|
assert result_plain.exit_code == 0
|
||||||
assert result_plain.output == "flag{test_cli_flag}"
|
assert result_plain.output.strip() == "flag{test_cli_flag}"
|
||||||
finally:
|
finally:
|
||||||
write_config(original_config, str(config_file))
|
write_config(original_config, str(config_file))
|
||||||
|
|
||||||
|
|
||||||
def test_flag_cmd_exemption(capsys):
|
def test_flag_cmd_exemption(capsys):
|
||||||
"""
|
"""
|
||||||
Verifies that running ctf flag is exempted from triggering the stdout flag warning box.
|
Verifies that running ctf flag is exempted from triggering the stdout flag warning box.
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# tests/test_steg.py
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import click
|
||||||
|
from click.testing import CliRunner
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ctf.steg import crack_steghide, SteghideCrackResult
|
||||||
|
|
||||||
|
def test_crack_steghide_missing_binary():
|
||||||
|
"""
|
||||||
|
Verifies that crack_steghide returns failure if steghide is not installed.
|
||||||
|
"""
|
||||||
|
with patch("shutil.which", return_value=None):
|
||||||
|
res = crack_steghide(Path("somefile.jpg"), ["pass1", "pass2"])
|
||||||
|
assert not res.success
|
||||||
|
assert "not found on system" in res.error_message
|
||||||
|
|
||||||
|
def test_crack_steghide_file_not_found():
|
||||||
|
"""
|
||||||
|
Verifies that crack_steghide returns failure if the input file does not exist.
|
||||||
|
"""
|
||||||
|
with patch("shutil.which", return_value="steghide"):
|
||||||
|
res = crack_steghide(Path("non_existent_file_12345.jpg"), ["pass1", "pass2"])
|
||||||
|
assert not res.success
|
||||||
|
assert "File not found" in res.error_message
|
||||||
|
|
||||||
|
def test_crack_steghide_success():
|
||||||
|
"""
|
||||||
|
Verifies that crack_steghide returns success, the password, and payload when cracked.
|
||||||
|
"""
|
||||||
|
wordlist = ["wrong1", "correct_pass", "wrong2"]
|
||||||
|
|
||||||
|
# We want to mock subprocess.run to write a payload to the out_file (-xf)
|
||||||
|
# when the correct password is tried.
|
||||||
|
def mock_run(cmd, *args, **kwargs):
|
||||||
|
# cmd: ['steghide', 'extract', '-sf', '...', '-p', password, '-xf', out_file, '-f']
|
||||||
|
password = cmd[5]
|
||||||
|
out_file = cmd[7]
|
||||||
|
mock_res = MagicMock()
|
||||||
|
if password == "correct_pass":
|
||||||
|
mock_res.returncode = 0
|
||||||
|
# Write a mock payload file
|
||||||
|
Path(out_file).write_bytes(b"flag{steghide_cracked_payload}")
|
||||||
|
else:
|
||||||
|
mock_res.returncode = 1
|
||||||
|
return mock_res
|
||||||
|
|
||||||
|
with patch("shutil.which", return_value="steghide"), \
|
||||||
|
patch("subprocess.run", side_effect=mock_run), \
|
||||||
|
patch("pathlib.Path.exists", return_value=True):
|
||||||
|
|
||||||
|
res = crack_steghide(Path("mock_image.jpg"), wordlist)
|
||||||
|
assert res.success
|
||||||
|
assert res.password == "correct_pass"
|
||||||
|
assert res.payload == b"flag{steghide_cracked_payload}"
|
||||||
|
assert res.error_message is None
|
||||||
|
|
||||||
|
def test_crack_steghide_failure():
|
||||||
|
"""
|
||||||
|
Verifies that crack_steghide returns failure if the wordlist is exhausted.
|
||||||
|
"""
|
||||||
|
wordlist = ["wrong1", "wrong2"]
|
||||||
|
|
||||||
|
def mock_run(cmd, *args, **kwargs):
|
||||||
|
mock_res = MagicMock()
|
||||||
|
mock_res.returncode = 1
|
||||||
|
return mock_res
|
||||||
|
|
||||||
|
with patch("shutil.which", return_value="steghide"), \
|
||||||
|
patch("subprocess.run", side_effect=mock_run), \
|
||||||
|
patch("pathlib.Path.exists", return_value=True):
|
||||||
|
|
||||||
|
res = crack_steghide(Path("mock_image.jpg"), wordlist)
|
||||||
|
assert not res.success
|
||||||
|
assert res.password is None
|
||||||
|
assert res.payload is None
|
||||||
|
assert "Password not found in wordlist" in res.error_message
|
||||||
|
|
||||||
|
def test_cli_crack_steghide_success(tmp_path):
|
||||||
|
"""
|
||||||
|
Verifies that the crack-steghide CLI command prints success panel and previews payload.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# Create temp wordlist file
|
||||||
|
wordlist_file = tmp_path / "words.txt"
|
||||||
|
wordlist_file.write_text("pass1\npass2\n")
|
||||||
|
|
||||||
|
mock_file = tmp_path / "mock.jpg"
|
||||||
|
mock_file.write_text("dummy")
|
||||||
|
|
||||||
|
mock_result = SteghideCrackResult(
|
||||||
|
success=True,
|
||||||
|
password="pass2",
|
||||||
|
payload=b"flag{cli_steg_preview}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("ctf.cli_steg.crack_steghide", return_value=mock_result):
|
||||||
|
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file)])
|
||||||
|
assert res.exit_code == 0
|
||||||
|
assert "Successfully cracked!" in res.output
|
||||||
|
assert "pass2" in res.output
|
||||||
|
assert "flag{cli_steg_preview}" in res.output
|
||||||
|
|
||||||
|
def test_cli_crack_steghide_success_output_file(tmp_path):
|
||||||
|
"""
|
||||||
|
Verifies that the crack-steghide CLI command writes the payload to a file when specified.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
wordlist_file = tmp_path / "words.txt"
|
||||||
|
wordlist_file.write_text("pass1\n")
|
||||||
|
|
||||||
|
mock_file = tmp_path / "mock.jpg"
|
||||||
|
mock_file.write_text("dummy")
|
||||||
|
|
||||||
|
out_file = tmp_path / "out.txt"
|
||||||
|
|
||||||
|
mock_result = SteghideCrackResult(
|
||||||
|
success=True,
|
||||||
|
password="pass1",
|
||||||
|
payload=b"written_file_content"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("ctf.cli_steg.crack_steghide", return_value=mock_result):
|
||||||
|
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file), "-o", str(out_file)])
|
||||||
|
assert res.exit_code == 0
|
||||||
|
assert "Successfully cracked!" in res.output
|
||||||
|
assert out_file.exists()
|
||||||
|
assert out_file.read_bytes() == b"written_file_content"
|
||||||
|
|
||||||
|
def test_cli_crack_steghide_failure(tmp_path):
|
||||||
|
"""
|
||||||
|
Verifies that the crack-steghide CLI command displays correct error message on failure.
|
||||||
|
"""
|
||||||
|
from ctf.main import cli
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
wordlist_file = tmp_path / "words.txt"
|
||||||
|
wordlist_file.write_text("pass1\n")
|
||||||
|
|
||||||
|
mock_file = tmp_path / "mock.jpg"
|
||||||
|
mock_file.write_text("dummy")
|
||||||
|
|
||||||
|
mock_result = SteghideCrackResult(
|
||||||
|
success=False,
|
||||||
|
error_message="Password not found in wordlist."
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("ctf.cli_steg.crack_steghide", return_value=mock_result):
|
||||||
|
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file)])
|
||||||
|
assert res.exit_code == 0
|
||||||
|
assert "Cracking failed: Password not found in wordlist" in res.output
|
||||||
@@ -69,7 +69,7 @@ def test_suggest_patterns_happy_path_ctf():
|
|||||||
# Unanchored options:
|
# Unanchored options:
|
||||||
assert "[A-Z]{3}\\{[a-z]{4}_[a-z]{4}_\\d{3}\\}" in patterns
|
assert "[A-Z]{3}\\{[a-z]{4}_[a-z]{4}_\\d{3}\\}" in patterns
|
||||||
assert "[A-Z]+\\{[a-z]+_[a-z]+_\\d+\\}" in patterns
|
assert "[A-Z]+\\{[a-z]+_[a-z]+_\\d+\\}" in patterns
|
||||||
assert "[A-Za-z0-9_\\{\\}]+" in patterns
|
assert "[A-Za-z0-9_{}]+" in patterns
|
||||||
assert "CTF\\{easy_flag_123\\}" in patterns
|
assert "CTF\\{easy_flag_123\\}" in patterns
|
||||||
|
|
||||||
# Anchored options:
|
# Anchored options:
|
||||||
@@ -131,8 +131,8 @@ def test_suggest_patterns_with_competition_name_non_braced():
|
|||||||
|
|
||||||
assert "SKY-\\d{4}-\\d{3}" in patterns
|
assert "SKY-\\d{4}-\\d{3}" in patterns
|
||||||
assert "^SKY-\\d{4}-\\d{3}$" in patterns
|
assert "^SKY-\\d{4}-\\d{3}$" in patterns
|
||||||
assert "SKY-.*" in patterns
|
assert "SKY.*" in patterns
|
||||||
assert "^SKY-.*$" in patterns
|
assert "^SKY.*$" in patterns
|
||||||
|
|
||||||
def test_suggest_patterns_with_competition_name_mismatch():
|
def test_suggest_patterns_with_competition_name_mismatch():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,6 +2,110 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base58"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chepy"
|
||||||
|
version = "7.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "base58" },
|
||||||
|
{ name = "colorama" },
|
||||||
|
{ name = "crccheck" },
|
||||||
|
{ name = "decorator" },
|
||||||
|
{ name = "docstring-parser" },
|
||||||
|
{ name = "emoji" },
|
||||||
|
{ name = "exrex" },
|
||||||
|
{ name = "fire" },
|
||||||
|
{ name = "hexdump" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "jsonpickle" },
|
||||||
|
{ name = "lazy-import" },
|
||||||
|
{ name = "lz4" },
|
||||||
|
{ name = "msgpack" },
|
||||||
|
{ name = "parsel" },
|
||||||
|
{ name = "passlib" },
|
||||||
|
{ name = "pgpy" },
|
||||||
|
{ name = "pretty-errors" },
|
||||||
|
{ name = "prompt-toolkit" },
|
||||||
|
{ name = "pycipher" },
|
||||||
|
{ name = "pycryptodome" },
|
||||||
|
{ name = "pydash" },
|
||||||
|
{ name = "pyopenssl" },
|
||||||
|
{ name = "pyperclip" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "regex" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3f/4c/b8bc1607c96e1df206912e4db82ace5adfb2fe9dce5926fd7860d265fa58/chepy-7.6.0.tar.gz", hash = "sha256:f6084689e43d128748c18f79c79d75539abb67ac8245e950b678cfa0da2f5010", size = 161603, upload-time = "2026-04-17T04:04:55.803Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.4.2"
|
version = "8.4.2"
|
||||||
@@ -23,11 +127,51 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crccheck"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3c/d1/a943f4f1ca899917cc3fe1cb89d59348edd1b407503e4b02608e8d6b421e/crccheck-1.3.1.tar.gz", hash = "sha256:1544c0110bf0a697d875d4f29dc40d7079f9d4d402a9317383f55f90ca72563a", size = 41696, upload-time = "2025-07-10T07:01:08.683Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/b5/68a9054be852e61f31de1be4e8d95802646b93344ce17c2380a1748706fa/crccheck-1.3.1-py3-none-any.whl", hash = "sha256:1680c9a7bb1ca4bec45fa19b8ca64319f10d2ce4eb8b0d25d51cb99a20ca0108", size = 24608, upload-time = "2025-07-10T07:01:07.25Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "41.0.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/b3/13a12ea7edb068de0f62bac88a8ffd92cc2901881b391839851846b84a81/cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", size = 630892, upload-time = "2023-11-28T00:49:41.784Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/73/5461318abd2fe426855a2f66775c063bbefd377729ece3c3ee048ddf19a5/cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", size = 5342676, upload-time = "2023-11-28T00:48:49.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/76/d705397d076fcbf5671544eb72a70b5a5ac83462d23dbd2a365a3bf3692a/cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", size = 2850425, upload-time = "2023-11-28T00:49:29.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/bb/475658ea92653a894589e657d6cea9ae01354db73405d62126ac5e74e2f8/cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", size = 4161547, upload-time = "2023-11-28T00:48:38.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/fd/dd5bd6ab0d12476ebca579cbfd48d31bd90fa28fa257b209df585dcf62a0/cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", size = 4363688, upload-time = "2023-11-28T00:49:34.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/81/ae2c51ea2b80d57d5756a12df67816230124faea0a762a7a6304fe3c819c/cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", size = 4148846, upload-time = "2023-11-28T00:49:04.704Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/bd/69628ab50368b1beb900eb1de5c46f8137169b75b2458affe95f2f470501/cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", size = 4372805, upload-time = "2023-11-28T00:48:44.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/4a/1808333c5ea79cb6d51102036cbcf698704b1fc7a5ccd139957aeadd2311/cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", size = 4255463, upload-time = "2023-11-28T00:48:32.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/07/826d66b6b03c5bfde8b451bea22c41e68d60aafff0ffa02c5f0819844319/cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", size = 4441232, upload-time = "2023-11-28T00:49:20.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/4f/11b739e95598db236013cc9efb4e3d02b51dd0861c85470c3fe42720ef5b/cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d", size = 2236277, upload-time = "2023-11-28T00:49:09.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/23/b28f4a03650512efff13a8fcbb977bac178a765c5a887a6720bee13fa85b/cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2", size = 2671839, upload-time = "2023-11-28T00:48:41.223Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssselect"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/2e/cdfd8b01c37cbf4f9482eefd455853a3cf9c995029a46acd31dfaa9c1dd6/cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92", size = 40589, upload-time = "2026-01-29T07:00:26.701Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540, upload-time = "2026-01-29T07:00:24.994Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctf"
|
name = "ctf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "chepy" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
@@ -41,6 +185,7 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "chepy" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "platformdirs", specifier = ">=4.9.4" },
|
{ name = "platformdirs", specifier = ">=4.9.4" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
@@ -50,6 +195,60 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pytest", specifier = ">=9.0.3" }]
|
dev = [{ name = "pytest", specifier = ">=9.0.3" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decorator"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docstring-parser"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "emoji"
|
||||||
|
version = "2.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/78/0d2db9382c92a163d7095fc08efff7800880f830a152cfced40161e7638d/emoji-2.15.0.tar.gz", hash = "sha256:eae4ab7d86456a70a00a985125a03263a5eac54cd55e51d7e184b1ed3b6757e4", size = 615483, upload-time = "2025-09-21T12:13:02.755Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/4b5aaaabddfacfe36ba7768817bd1f71a7a810a43705e531f3ae4c690767/emoji-2.15.0-py3-none-any.whl", hash = "sha256:205296793d66a89d88af4688fa57fd6496732eb48917a87175a023c8138995eb", size = 608433, upload-time = "2025-09-21T12:13:01.197Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exrex"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/26/9a/580635ea99178aba728de721e88e5a0d18b528805c58e8b26eab87be2a45/exrex-0.12.0.tar.gz", hash = "sha256:3f582add0700d4141e279625252e7a0655f9ad81feab3b8ab9077966668a2c99", size = 19766, upload-time = "2024-11-13T13:46:36.399Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/90/89fdfc6f426cc6414cc8e1c599c4333d800f8f3ff05cb6ce69c8d0eaa245/exrex-0.12.0-py3-none-any.whl", hash = "sha256:1228f2e3afb008cacf6adc3ec20b098f1303886f4382b57cdf3b7259fb7c0ad3", size = 23590, upload-time = "2024-11-13T13:46:34.647Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fire"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "termcolor" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hexdump"
|
||||||
|
version = "3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/b3/279b1d57fa3681725d0db8820405cdcb4e62a9239c205e4ceac4391c78e4/hexdump-3.3.zip", hash = "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db", size = 12658, upload-time = "2016-01-22T14:40:19.589Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -59,6 +258,93 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jmespath"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpickle"
|
||||||
|
version = "4.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8d/c0/dde9b4b42cc415b9579573f967f12efbb034e427a2a37e93ad5139891d87/jsonpickle-4.1.2.tar.gz", hash = "sha256:8afed18aa189fd81e2e833b426bb4af485594921f0b1d36c2001fc5637a2f210", size = 319120, upload-time = "2026-05-28T03:50:11.892Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/7b/fd3c7a09649aea9da1d3587aea624d8f9b29963dfd84a1bdb2aa93b36dac/jsonpickle-4.1.2-py3-none-any.whl", hash = "sha256:7ffe34426bc797684dbf1dc84185558bd864cd25b1ff5fb01b7405e392d0a937", size = 47203, upload-time = "2026-05-28T03:50:10.605Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-import"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/44/2e/5378f9b9cbc893826c2ecb022646c97ece9efbaad351adf89425fff33990/lazy_import-0.2.2.tar.gz", hash = "sha256:2149aef8579459407c62cfeccf118527939c9931ace124f355236360644f8a3d", size = 15265, upload-time = "2018-01-22T02:19:32.029Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "6.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz4"
|
||||||
|
version = "4.4.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/9c/70bdbdb9f54053a308b200b4678afd13efd0eafb6ddcbb7f00077213c2e5/lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f", size = 207586, upload-time = "2025-11-03T13:02:18.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/cb/bfead8f437741ce51e14b3c7d404e3a1f6b409c440bad9b8f3945d4c40a7/lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6", size = 207161, upload-time = "2025-11-03T13:02:19.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/18/b192b2ce465dfbeabc4fc957ece7a1d34aded0d95a588862f1c8a86ac448/lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9", size = 1292415, upload-time = "2025-11-03T13:02:20.829Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/79/a4e91872ab60f5e89bfad3e996ea7dc74a30f27253faf95865771225ccba/lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668", size = 1279920, upload-time = "2025-11-03T13:02:22.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/01/d52c7b11eaa286d49dae619c0eec4aabc0bf3cda7a7467eb77c62c4471f3/lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f", size = 1368661, upload-time = "2025-11-03T13:02:23.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/da/137ddeea14c2cb86864838277b2607d09f8253f152156a07f84e11768a28/lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67", size = 90139, upload-time = "2025-11-03T13:02:24.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/2c/8332080fd293f8337779a440b3a143f85e374311705d243439a3349b81ad/lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be", size = 101497, upload-time = "2025-11-03T13:02:25.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "4.2.0"
|
version = "4.2.0"
|
||||||
@@ -80,6 +366,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "msgpack"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.1"
|
version = "26.1"
|
||||||
@@ -89,6 +401,41 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parsel"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cssselect" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "lxml" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "w3lib" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/91/c8/4ace3a5c61e39ca21734a5715d0e076eea6200dd8daea2a5b99452f5a0d6/parsel-1.11.0.tar.gz", hash = "sha256:5925fe087eb16fc404a7ed91e31e2c1e2a9b230da4b64f34d81358c0d0e27e88", size = 106849, upload-time = "2026-01-29T07:19:23.388Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/23/4e0dae5e5bee14aea26dba003a682e621563451a20f751ed985810f818b6/parsel-1.11.0-py3-none-any.whl", hash = "sha256:bda82575df1774dd64e1c1396163f3cadb3e383e0f8080d43d45fa6705355daa", size = 14176, upload-time = "2026-01-29T07:19:22.255Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pgpy"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "pyasn1" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5e285ba9465550a48e837b08b27fa99ef238618f6085a1464afaa4dc8426/PGPy-0.6.0.tar.gz", hash = "sha256:279c2e353f4c3a319f00bd9bd582456e420f8a3ac6de2b4e9731444746828383", size = 603002, upload-time = "2022-11-24T02:35:22.133Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.9.6"
|
version = "4.9.6"
|
||||||
@@ -107,6 +454,85 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty-errors"
|
||||||
|
version = "1.2.25"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c8/a7/823f451d48726729b4983072aad7c442f4c02fbaec04b71c1f4c03993964/pretty_errors-1.2.25.tar.gz", hash = "sha256:a16ba5c752c87c263bf92f8b4b58624e3b1e29271a9391f564f12b86e93c6755", size = 20332, upload-time = "2021-11-24T14:32:10.529Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/8e/2df7467a15eae40e26c476683962fdb810cd1b36676603e2f139b4abbeaf/pretty_errors-1.2.25-py3-none-any.whl", hash = "sha256:8ce68ccd99e0f2a099265c8c1f1c23b7c60a15d69bb08816cb336e237d5dc983", size = 17195, upload-time = "2021-11-24T14:32:07.762Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prompt-toolkit"
|
||||||
|
version = "3.0.52"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "wcwidth" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycipher"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/44/8f/765d9889f76b21bf5327bea518786647e48bec1295a2478dd54e8efdb85d/pycipher-0.5.2.zip", hash = "sha256:e5b3b3ef734604da09e1642f9c496a75df850668aee5b18818a3988eec4cd71b", size = 45497, upload-time = "2016-07-14T05:12:51.907Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycryptodome"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydash"
|
||||||
|
version = "8.0.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.20.0"
|
||||||
@@ -116,6 +542,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyopenssl"
|
||||||
|
version = "23.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/df/75a6525d8988a89aed2393347e9db27a56cb38a3e864314fac223e905aef/pyOpenSSL-23.2.0.tar.gz", hash = "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac", size = 185132, upload-time = "2023-05-31T03:21:28.901Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/e2/f8b4f1c67933a4907e52228241f4bd52169f3196b70af04403b29c63238a/pyOpenSSL-23.2.0-py3-none-any.whl", hash = "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2", size = 59036, upload-time = "2023-05-31T03:21:26.479Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyperclip"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.3"
|
version = "9.0.3"
|
||||||
@@ -132,6 +579,72 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2026.7.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/37/451aaddbf50922f34d744ad5ca919ae1fcfac112123885d9728f52a484b3/regex-2026.7.10.tar.gz", hash = "sha256:1050fedf0a8a92e843971120c2f57c3a99bea86c0dfa1d63a9fac053fe54b135", size = 416282, upload-time = "2026-07-10T19:49:46.267Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/4a/a7fa3ada9bd2d2ce20d56dfceec6b2a51afeed9bf3d8286355ceec5f0628/regex-2026.7.10-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:39f81d1fdf594446495f2f4edd8e62d8eda0f7a802c77ac596dc8448ad4cc5ca", size = 497087, upload-time = "2026-07-10T19:48:40.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/7e/ca0b1a87192e5828dbc16f16ae6caca9b67f25bf729a3348468a5ff52755/regex-2026.7.10-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:441edc66a54063f8269d1494fc8474d06605e71e8a918f4bcfd079ebda4ce042", size = 297307, upload-time = "2026-07-10T19:48:42.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6c/fb40bb34275d3cd4d7a376d5fb2ea1f0f4a96fd884fa83c0c4ae869001bf/regex-2026.7.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cfeb11990f59e59a0df26c648f0adfcbf27be77241250636f5769eb08db662be", size = 292163, upload-time = "2026-07-10T19:48:43.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/0b/34cbea16c8fea9a18475a7e8f5837c70af451e738bfeb4eb5b029b7dc07a/regex-2026.7.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:460176b2db044a292baaee6891106566739657877af89a251cded228689015a6", size = 797064, upload-time = "2026-07-10T19:48:45.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/77/f6805d97f15f5a710bdfd56a768f3468c978239daf9e1b15efd8935e1967/regex-2026.7.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9dc55698737aca028848bde418d6c51d74f2a5fd44872d3c8b56b626729adb89", size = 866155, upload-time = "2026-07-10T19:48:47.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/e3/a2a905807bba3bcd90d6ebbb67d27af2adf7d41708175cbc6b956a0c75f1/regex-2026.7.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3e10779f60c000213a5b53f518824bd07b3dc119333b26d70c6be1c27b5c794", size = 911596, upload-time = "2026-07-10T19:48:49.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/ca/a3126888b2c6f33c7e29144fedf85f6d5a52a400024fa045ad8fc0550ef1/regex-2026.7.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38a5926601aaccf379512746b86eb0ac1d29121f6c776dac6ac5b31077432f2c", size = 800713, upload-time = "2026-07-10T19:48:51.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/19/9d252fd969f726c8b56b4bacf910811cc70495a110907b3a7ccb96cd9cad/regex-2026.7.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a72ecf5bfd3fc8d57927f7e3ded2487e144472f39010c3acaec3f6f3ff53f361", size = 777286, upload-time = "2026-07-10T19:48:53.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/7a/5f1bf433fa446ecb3aab87bb402603dc9e171ef8052c1bb8690bb4e255a3/regex-2026.7.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d50714405845c1010c871098558cfe5718fe39d2a2fab5f95c8863caeb7a82b3", size = 785826, upload-time = "2026-07-10T19:48:55.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/ca/69f3a7281d86f1b592338007f3e535cc219d771448e2b61c0b56e4f9d05b/regex-2026.7.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ec1c44cf9bd22079aac37a07cb49a29ced9050ab5bddf24e50aba298f1e34d90", size = 860957, upload-time = "2026-07-10T19:48:57.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/11/487ff55c8d515ec9dd60d7ba3c129eeaa9e527358ed9e8a054a9e9430f81/regex-2026.7.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9e9aaef25a40d1f1e1bbb1d0eb0190c4a64a7a1750f7eb67b8399bed6f4fd2a6", size = 765959, upload-time = "2026-07-10T19:49:00.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e1/fa034e6fa8896a09bd0d5e19c81fdc024411ab37980950a0401dccee8f6d/regex-2026.7.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e54e088dc64dd2766014e7cfe5f8bc45399400fd486816e494f93e3f0f55da06", size = 851447, upload-time = "2026-07-10T19:49:02.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/a5/b9427ed53b0e14c540dc436d56aaf57a19fb9183c6e7abd66f4b4368fbad/regex-2026.7.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:834271b1ff2cfa1f67fcd65a48bf11d11e9ab837e21bf79ce554efb648599ae8", size = 789418, upload-time = "2026-07-10T19:49:03.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/52/aab92420c8aa845c7bcbe68dc65023d4a9e9ea785abf0beb2198f0de5ba1/regex-2026.7.10-cp314-cp314-win32.whl", hash = "sha256:f988a1cec68058f71a38471813fba9e87dffe855582682e8a10e40ece12567a2", size = 272538, upload-time = "2026-07-10T19:49:05.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/16/5c7050e0ef7dd8889441924ff0a2c33b7f0587c0ccb0953fe7ca997d673b/regex-2026.7.10-cp314-cp314-win_amd64.whl", hash = "sha256:2129e4a5e86f26926982d883dff815056f2e98220fdf630e59f961b578a26c43", size = 280796, upload-time = "2026-07-10T19:49:07.593Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/1a/4f6099d2ba271502fdb97e697bae2ed0213c0d87f2273fe7d21e2e401d12/regex-2026.7.10-cp314-cp314-win_arm64.whl", hash = "sha256:9cd5b6805396157b4cf993a6940cbb8663161f29b4df2458c1c9991f099299c5", size = 281017, upload-time = "2026-07-10T19:49:09.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/02/4061fc71f64703e0df61e782c2894c3fbc089d277767eff6e16099581c73/regex-2026.7.10-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:103e8f3acc3dcede88c0331c8612766bdcfc47c9250c5477f0e10e0550b9da49", size = 501467, upload-time = "2026-07-10T19:49:11.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/a5/8d42b2f3fd672908a05582effd0f88438bf9bb4e8e02d69a62c723e23601/regex-2026.7.10-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:538ddb143f5ca085e372def17ef3ed9d74b50ad7fc431bd85dc50a9af1a7076f", size = 299700, upload-time = "2026-07-10T19:49:14.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/70/36fa4b46f73d268c0dbe77c40e62da2cd4833ee206d3b2e438c2034e1f36/regex-2026.7.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e3448e86b05ce87d4eb50f9c680860830f3b32493660b39f43957d6263e2eba", size = 294590, upload-time = "2026-07-10T19:49:15.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/a7/b6db1823f3a233c2a46f854fdc986f4fd424a84ed557b7751f2998efb266/regex-2026.7.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eab9d3f981c423afd1a61db055cfe83553c3f6455949e334db04722469dd0a2", size = 811925, upload-time = "2026-07-10T19:49:17.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/7d/f8bee4c210c42c7e8b952bb9fb7099dd7fb2f4bd0f33d0d65a8ab08aafc0/regex-2026.7.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:177f930af3ad72e1045f8877540e0c43a38f7d328cf05f31963d0bd5f7ecf067", size = 871257, upload-time = "2026-07-10T19:49:19.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/78/22adf72e614ba0216b996e9aaef5712c23699e360ea127bb3d5ee1a7666f/regex-2026.7.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd3b6d97beb39afb412f2c79522b9e099463c31f4c49ab8347c5a2ca3531c478", size = 917551, upload-time = "2026-07-10T19:49:22.069Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/f7/ebc15a39e81e6b58da5f913b91fc293a25c6700d353c14d5cd25fc85712a/regex-2026.7.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8679f0652a183d93da646fcec8da8228db0be40d1595da37e6d74c2dc8c4713c", size = 816436, upload-time = "2026-07-10T19:49:24.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/33/20bc2bdd57f7e0fcc51be37e4c4d1bca7f0b4af8dc0a148c23220e689da8/regex-2026.7.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:494b19a5805438aeb582de99f9d97603d8fd48e6f4cc74d0088bb292b4da3b70", size = 785935, upload-time = "2026-07-10T19:49:26.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/51/87ff99c849b56309c40214a72b54b0eef320d0516a8a516970cc8be1b725/regex-2026.7.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0911e34151a5429d0325dae538ba9851ec0b62426bdfd613060cda8f1c36ec7f", size = 801494, upload-time = "2026-07-10T19:49:28.493Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/11/fde67d49083fef489b7e0f841e2e5736516795b166c9867f05956c1e494b/regex-2026.7.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b862572b7a5f5ed47d2ba5921e63bf8d9e3b682f859d8f11e0e5ca46f7e82173", size = 866549, upload-time = "2026-07-10T19:49:30.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/b5/31a156c36acf10181d88f55a66c688d5454a344e53ccc03d49f4a48a2297/regex-2026.7.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3f361215e000d68a4aff375106637b83c80be36091d83ee5107ad3b32bd73f48", size = 773089, upload-time = "2026-07-10T19:49:32.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/bb/734e978c904726664df47ae36ce5eca5065de5141185ae46efec063476a2/regex-2026.7.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4533af6099543db32ef26abc2b2f824781d4eebb309ab9296150fd1a0c7eb07d", size = 856710, upload-time = "2026-07-10T19:49:35.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/e5/dc35cea074dbdcb9776c4b0542a3bc326ff08454af0768ef35f3fc66e7fa/regex-2026.7.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:668ab85105361d0200e3545bec198a1acfc6b0aeb5fff8897647a826e5a171be", size = 803621, upload-time = "2026-07-10T19:49:37.704Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/b2/124564af46bc0b592785610b3985315610af0a07f4cf21fa36e06c2398dd/regex-2026.7.10-cp314-cp314t-win32.whl", hash = "sha256:dd7715817a187edd7e2a2390908757f7ba42148e59cad755fb8ee1160c628eca", size = 274558, upload-time = "2026-07-10T19:49:39.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/9c/cd813ce9f3404c0443915175c1e339c5afd8fcda04310102eaf233015eef/regex-2026.7.10-cp314-cp314t-win_amd64.whl", hash = "sha256:78712d4954234df5ca24fdadb65a2ab034213f0cdfde376c272f9fc5e09866bb", size = 283687, upload-time = "2026-07-10T19:49:41.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/d3/3dae6a6ce46144940e64425e32b8573a393a009aeaf75fa6752a35399056/regex-2026.7.10-cp314-cp314t-win_arm64.whl", hash = "sha256:749b92640e1970e881fdf22a411d74bf9d049b154f4ef7232eeb9a90dd8be7f3", size = 283377, upload-time = "2026-07-10T19:49:43.985Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "15.0.0"
|
version = "15.0.0"
|
||||||
@@ -145,6 +658,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "83.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -153,3 +693,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "w3lib"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/91/b2eb59c2cf243de5de1e91c963655df78c015509f51297685a8c86a27b8c/w3lib-2.4.1.tar.gz", hash = "sha256:8dd69ee39ff6398d708c793abc779c334a69bac7cee1cdf71736c669ed6be864", size = 48494, upload-time = "2026-03-20T09:50:27.477Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/c3/f8b216cbd742e5b84c40f045204c764ccb7524d2aeab021054ec69446b0a/w3lib-2.4.1-py3-none-any.whl", hash = "sha256:40930132907e68de906a5b89331ab8c8ff4f01bd35b5539ef7896017d814138d", size = 21695, upload-time = "2026-03-20T09:50:26.187Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wcwidth"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
||||||
|
]
|
||||||
|
|||||||