flagonfly 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flagonfly-0.0.1/PKG-INFO +90 -0
- flagonfly-0.0.1/README.md +79 -0
- flagonfly-0.0.1/pyproject.toml +31 -0
- flagonfly-0.0.1/src/flagonfly/__init__.py +1 -0
- flagonfly-0.0.1/src/flagonfly/__main__.py +4 -0
- flagonfly-0.0.1/src/flagonfly/cli.py +11 -0
- flagonfly-0.0.1/src/flagonfly/cli_complete.py +54 -0
- flagonfly-0.0.1/src/flagonfly/cli_message_constants.py +4 -0
- flagonfly-0.0.1/src/flagonfly/commands/__init__.py +0 -0
- flagonfly-0.0.1/src/flagonfly/commands/codec_commands.py +233 -0
- flagonfly-0.0.1/src/flagonfly/core/__init__.py +0 -0
- flagonfly-0.0.1/src/flagonfly/core/codec.py +119 -0
- flagonfly-0.0.1/src/flagonfly/gui.py +11 -0
flagonfly-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flagonfly
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: aidnem
|
|
6
|
+
Author-email: aidnem <aidnem@noreply.codeberg.org>
|
|
7
|
+
Requires-Dist: bitstring>=4.4.0
|
|
8
|
+
Requires-Dist: click>=8.4.1
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# flagonfly
|
|
13
|
+
|
|
14
|
+
The ultimate CTF tool. Designed to combat the staggering lack of cross-platform CTF tools. Named after the ultimate hunter, the dragonfly.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
For now, flagonfly is not published to any package repositories. To install it, use `pip` or `uv`:
|
|
19
|
+
|
|
20
|
+
1. First, clone the repository somewhere convenient. Change directory into the project:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
git clone https://codeberg.org/aidnem/flagonfly.git
|
|
24
|
+
cd flagonfly
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Install the package using your package manager of choice.
|
|
28
|
+
|
|
29
|
+
It's preferable to use a package manager that will install the project in its own isolated environment, such as `uv tool install` or `pipx install`.
|
|
30
|
+
|
|
31
|
+
Install the project in editable mode (use the `-e` flag) so that it can be updated by running `git pull` at a later time:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv tool install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Alternatively, use pipx:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pipx install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Finally, if you prefer pip, you can use that instead. Keep in mind that this will install the dependencies to your global pip environment:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Installing using any of these 3 methods will add 3 executables to your python script path:
|
|
50
|
+
|
|
51
|
+
- `flagonfly` - The flagonfly CLI. This is how all CLI tools can be used.
|
|
52
|
+
- `flf` - A convenient alias for `flagonfly`. Because the command is intended to be deeply composed (e.g. `cat data | flf encode binary | rev | flf decode binary`), a shorter alias is useful in saving time composing commands.
|
|
53
|
+
- `flagonfly-gui` - Launches the flagonfly GUI, a tkinter app that is intended to provide some of the functionality of the flagonfly library in a graphical form.
|
|
54
|
+
|
|
55
|
+
## Core Library
|
|
56
|
+
|
|
57
|
+
The flagonfly project is separated into core, UI, and CLI projects so that the central functionality of the can be used from other projects. Please note that, during early development, doc comments will likely be sparse. To use the functionality of flagonfly in other projects, look into importing necessary functions and packages from `flagonfly.core`.
|
|
58
|
+
|
|
59
|
+
## CLI: Subcommands
|
|
60
|
+
|
|
61
|
+
### Help
|
|
62
|
+
|
|
63
|
+
Flagonfly contains detailed help messages for commands and their parameters wherever possible. Use the `--help` flag on any command to see detailed descriptions of the arguments.
|
|
64
|
+
|
|
65
|
+
This is important because this README will likely be updated less often and less thoroughly than the help messages themselves.
|
|
66
|
+
|
|
67
|
+
### Encode/decode
|
|
68
|
+
|
|
69
|
+
Use `flagonfly encode` or `flagonfly decode` followed by a format to convert data to different formats.
|
|
70
|
+
|
|
71
|
+
For example, `echo "Hello, world!" | flagonfly encode binary` will encode the ASCII text into binary and output it as a string of "1"s and "0"s. The `decode` command does the reverse.
|
|
72
|
+
|
|
73
|
+
If an encoding isn't specified when decoding, flagonfly will attempt to guess at what encoding was used. It does this by attempting to decode using various encodings in order until one succeeds. This is not foolproof; if you data was encoded in a larger format but using only certain digits (e.g. data encoded as hex that only uses 1s and 0s), it will decode incorrectly. However, it's good enough as a first try for decoding data quickly.
|
|
74
|
+
|
|
75
|
+
#### Words and separators
|
|
76
|
+
|
|
77
|
+
By default, the output of `encode` is divided into words, separated by a separator string. There are sensible defaults for all data types, and the separator/word length parameters are ignored when encoding to base64/32/16.
|
|
78
|
+
|
|
79
|
+
To change the separator, pass a string to the `-s`/`--separator` option. To remove the separator, pass an empty string: `flagonfly encode -s "" ...`.
|
|
80
|
+
|
|
81
|
+
To change the word size, use the `-w` or `--word-length` option.
|
|
82
|
+
|
|
83
|
+
#### Prefixes
|
|
84
|
+
The `-p`/`--prefix` option can be used to append a prefix (e.g. `0b` for binary data). When using no separator, this prefix is appended to the beginning of the output. When using a separator between words, it will be appended to the beginning of each word.
|
|
85
|
+
|
|
86
|
+
There is no prefix for base64/32/16 data.
|
|
87
|
+
|
|
88
|
+
## CI Usage
|
|
89
|
+
|
|
90
|
+
The flagonfly project will hopefully leverage Codeberg's hosted woodpecker instance for continuous integration. Its needs are fairly bare-bones: running unit tests on all PRs to main, and running an automatic publish workflow to publish the project to PyPI automatically whenever a new release is tagged. The processing, memory, and storage requirements of these workflows should be minimal, as the project will hopefully remain quite small.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# flagonfly
|
|
2
|
+
|
|
3
|
+
The ultimate CTF tool. Designed to combat the staggering lack of cross-platform CTF tools. Named after the ultimate hunter, the dragonfly.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
For now, flagonfly is not published to any package repositories. To install it, use `pip` or `uv`:
|
|
8
|
+
|
|
9
|
+
1. First, clone the repository somewhere convenient. Change directory into the project:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone https://codeberg.org/aidnem/flagonfly.git
|
|
13
|
+
cd flagonfly
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. Install the package using your package manager of choice.
|
|
17
|
+
|
|
18
|
+
It's preferable to use a package manager that will install the project in its own isolated environment, such as `uv tool install` or `pipx install`.
|
|
19
|
+
|
|
20
|
+
Install the project in editable mode (use the `-e` flag) so that it can be updated by running `git pull` at a later time:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
uv tool install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Alternatively, use pipx:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pipx install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Finally, if you prefer pip, you can use that instead. Keep in mind that this will install the dependencies to your global pip environment:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Installing using any of these 3 methods will add 3 executables to your python script path:
|
|
39
|
+
|
|
40
|
+
- `flagonfly` - The flagonfly CLI. This is how all CLI tools can be used.
|
|
41
|
+
- `flf` - A convenient alias for `flagonfly`. Because the command is intended to be deeply composed (e.g. `cat data | flf encode binary | rev | flf decode binary`), a shorter alias is useful in saving time composing commands.
|
|
42
|
+
- `flagonfly-gui` - Launches the flagonfly GUI, a tkinter app that is intended to provide some of the functionality of the flagonfly library in a graphical form.
|
|
43
|
+
|
|
44
|
+
## Core Library
|
|
45
|
+
|
|
46
|
+
The flagonfly project is separated into core, UI, and CLI projects so that the central functionality of the can be used from other projects. Please note that, during early development, doc comments will likely be sparse. To use the functionality of flagonfly in other projects, look into importing necessary functions and packages from `flagonfly.core`.
|
|
47
|
+
|
|
48
|
+
## CLI: Subcommands
|
|
49
|
+
|
|
50
|
+
### Help
|
|
51
|
+
|
|
52
|
+
Flagonfly contains detailed help messages for commands and their parameters wherever possible. Use the `--help` flag on any command to see detailed descriptions of the arguments.
|
|
53
|
+
|
|
54
|
+
This is important because this README will likely be updated less often and less thoroughly than the help messages themselves.
|
|
55
|
+
|
|
56
|
+
### Encode/decode
|
|
57
|
+
|
|
58
|
+
Use `flagonfly encode` or `flagonfly decode` followed by a format to convert data to different formats.
|
|
59
|
+
|
|
60
|
+
For example, `echo "Hello, world!" | flagonfly encode binary` will encode the ASCII text into binary and output it as a string of "1"s and "0"s. The `decode` command does the reverse.
|
|
61
|
+
|
|
62
|
+
If an encoding isn't specified when decoding, flagonfly will attempt to guess at what encoding was used. It does this by attempting to decode using various encodings in order until one succeeds. This is not foolproof; if you data was encoded in a larger format but using only certain digits (e.g. data encoded as hex that only uses 1s and 0s), it will decode incorrectly. However, it's good enough as a first try for decoding data quickly.
|
|
63
|
+
|
|
64
|
+
#### Words and separators
|
|
65
|
+
|
|
66
|
+
By default, the output of `encode` is divided into words, separated by a separator string. There are sensible defaults for all data types, and the separator/word length parameters are ignored when encoding to base64/32/16.
|
|
67
|
+
|
|
68
|
+
To change the separator, pass a string to the `-s`/`--separator` option. To remove the separator, pass an empty string: `flagonfly encode -s "" ...`.
|
|
69
|
+
|
|
70
|
+
To change the word size, use the `-w` or `--word-length` option.
|
|
71
|
+
|
|
72
|
+
#### Prefixes
|
|
73
|
+
The `-p`/`--prefix` option can be used to append a prefix (e.g. `0b` for binary data). When using no separator, this prefix is appended to the beginning of the output. When using a separator between words, it will be appended to the beginning of each word.
|
|
74
|
+
|
|
75
|
+
There is no prefix for base64/32/16 data.
|
|
76
|
+
|
|
77
|
+
## CI Usage
|
|
78
|
+
|
|
79
|
+
The flagonfly project will hopefully leverage Codeberg's hosted woodpecker instance for continuous integration. Its needs are fairly bare-bones: running unit tests on all PRs to main, and running an automatic publish workflow to publish the project to PyPI automatically whenever a new release is tagged. The processing, memory, and storage requirements of these workflows should be minimal, as the project will hopefully remain quite small.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flagonfly"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "aidnem", email = "aidnem@noreply.codeberg.org" }]
|
|
7
|
+
requires-python = ">=3.13"
|
|
8
|
+
dependencies = ["bitstring>=4.4.0", "click>=8.4.1"]
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
flagonfly = "flagonfly.cli:main"
|
|
12
|
+
flf = "flagonfly.cli:main"
|
|
13
|
+
|
|
14
|
+
[project.gui-scripts]
|
|
15
|
+
flagonfly-gui = "flagonfly.gui:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"mypy>=2.1.0",
|
|
24
|
+
"pytest>=9.1.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[tool.uv.index]]
|
|
28
|
+
name = "testpypi"
|
|
29
|
+
url = "https://test.pypi.org/simple/"
|
|
30
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
31
|
+
explicit = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""flagonfly CLI completion utility, for automatically picking an option based on a partial string"""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def complete_one_strict(inp: str, options: Iterable[str]) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Exactly like complete_one, but exits instead of returning None if a match isn't found.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
result = complete_one(inp, options)
|
|
15
|
+
|
|
16
|
+
if result is None:
|
|
17
|
+
click.echo(
|
|
18
|
+
f"[ERROR] Input '{inp}' did not match any of {options}.",
|
|
19
|
+
file=sys.stderr,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def complete_one(inp: str, options: Iterable[str]) -> str | None:
|
|
28
|
+
"""
|
|
29
|
+
Given an input string and an Iterable of options, pick the first option that starts with the input string.
|
|
30
|
+
|
|
31
|
+
If the input is an exact match, returns that immediately.
|
|
32
|
+
|
|
33
|
+
Prints a warning if multiple options are available.
|
|
34
|
+
|
|
35
|
+
If no matches are found, returns None.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if inp in options:
|
|
39
|
+
return inp
|
|
40
|
+
|
|
41
|
+
filtered_options = list(filter(lambda option: option.startswith(inp), options))
|
|
42
|
+
|
|
43
|
+
if len(filtered_options) == 0:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
selected_option = filtered_options[0]
|
|
47
|
+
|
|
48
|
+
if len(filtered_options) != 1:
|
|
49
|
+
click.echo(
|
|
50
|
+
f"[WARNING] Ambigious input '{inp}' matches multiple options ({filtered_options}), so first option was selected: '{selected_option}'.",
|
|
51
|
+
file=sys.stderr,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return selected_option
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""flagonfly's reusable help messages for common parameters that will be used between multiple subcommands"""
|
|
2
|
+
|
|
3
|
+
INPUT_FILE_HELP = "File to read for input. Defaults to `-`, meaning stdin."
|
|
4
|
+
OUTPUT_FILE_HELP = "File where output will be written. Defaults to `-`, meaning stdout."
|
|
File without changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flagonfly codex tools CLI bindings
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import IO, Callable
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from flagonfly.cli_message_constants import INPUT_FILE_HELP, OUTPUT_FILE_HELP
|
|
14
|
+
from flagonfly.core import codec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Codec:
|
|
19
|
+
encode_func: Callable[[bytes, bool, str, int], str]
|
|
20
|
+
decode_func: Callable[[str, str], bytes | None]
|
|
21
|
+
default_word_length: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Encoding(StrEnum):
|
|
25
|
+
BINARY = "binary"
|
|
26
|
+
HEX = "hex"
|
|
27
|
+
BASE64 = "b64"
|
|
28
|
+
URLSAFE_BASE64 = "b64url"
|
|
29
|
+
BASE32 = "b32"
|
|
30
|
+
BASE16 = "b16"
|
|
31
|
+
GUESS = "guess"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def guess_encode_wrapper(
|
|
35
|
+
data: bytes, prefix: bool, separator: str, word_length: int
|
|
36
|
+
) -> str:
|
|
37
|
+
click.echo(
|
|
38
|
+
"[WARN] Best-guess is not supported while encoding; using binary instead",
|
|
39
|
+
file=sys.stderr,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return codec.binary_encode(data, prefix, separator, word_length)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def wrap_encode_func_ignoring_separator(
|
|
46
|
+
encode_func: Callable[[bytes], str],
|
|
47
|
+
) -> Callable[[bytes, bool, str, int], str]:
|
|
48
|
+
"""
|
|
49
|
+
Given an encoding function that takes bytes and returns a string, wrap it in a function that accepts but ignores all other encoding arguments.
|
|
50
|
+
|
|
51
|
+
This function exists to provide an easy way to make base64 functions fit the interface required for CLI commands.
|
|
52
|
+
"""
|
|
53
|
+
return lambda data, _prefix, _separator, _word_length: encode_func(data)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def wrap_b64encode(
|
|
57
|
+
encode_func: Callable[[bytes], bytes],
|
|
58
|
+
) -> Callable[[bytes, bool, str, int], str]:
|
|
59
|
+
return wrap_encode_func_ignoring_separator(lambda data: encode_func(data).decode())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def wrap_b64decode(
|
|
63
|
+
decode_func: Callable[[str], bytes],
|
|
64
|
+
) -> Callable[[str, str], bytes]:
|
|
65
|
+
"""
|
|
66
|
+
Given a ecoding function that takes a string and returns bytes, wrap it in a function that accepts but ignores all other decoding arguments.
|
|
67
|
+
|
|
68
|
+
This function exists to provide an easy way to make base64 functions fit the interface required for CLI commands.
|
|
69
|
+
"""
|
|
70
|
+
return lambda data, _: decode_func(data)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ENCODINGS: dict[Encoding, Codec] = {
|
|
74
|
+
Encoding.BINARY: Codec(codec.binary_encode, codec.binary_decode, 8),
|
|
75
|
+
Encoding.HEX: Codec(codec.hex_encode, codec.hex_decode, 2),
|
|
76
|
+
Encoding.BASE64: Codec(
|
|
77
|
+
wrap_b64encode(base64.b64encode),
|
|
78
|
+
wrap_b64decode(lambda data: base64.b64decode(data)),
|
|
79
|
+
1,
|
|
80
|
+
),
|
|
81
|
+
Encoding.URLSAFE_BASE64: Codec(
|
|
82
|
+
wrap_b64encode(base64.urlsafe_b64encode),
|
|
83
|
+
wrap_b64decode(lambda data: base64.urlsafe_b64decode(data)),
|
|
84
|
+
1,
|
|
85
|
+
),
|
|
86
|
+
Encoding.BASE32: Codec(
|
|
87
|
+
wrap_b64encode(base64.b32encode),
|
|
88
|
+
wrap_b64decode(lambda data: base64.b32decode(data)),
|
|
89
|
+
1,
|
|
90
|
+
),
|
|
91
|
+
Encoding.BASE16: Codec(
|
|
92
|
+
wrap_b64encode(base64.b16encode),
|
|
93
|
+
wrap_b64decode(lambda data: base64.b16decode(data)),
|
|
94
|
+
1,
|
|
95
|
+
),
|
|
96
|
+
Encoding.GUESS: Codec(guess_encode_wrapper, codec.guess_decode, 8),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
SEPARATOR_HELP = "Separator placed between words (ignored in base64/32/16). Defaults to a single space."
|
|
100
|
+
INVALID_ENCODING_MSG = f"[ERROR] Invalid encoding chosen. Please choose one of {[e.value for e in Encoding]}."
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@click.command()
|
|
104
|
+
@click.argument(
|
|
105
|
+
"encoding",
|
|
106
|
+
type=click.Choice(
|
|
107
|
+
[e.value for e in Encoding],
|
|
108
|
+
case_sensitive=False,
|
|
109
|
+
),
|
|
110
|
+
default=Encoding.BINARY,
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"-i",
|
|
114
|
+
"--input",
|
|
115
|
+
type=click.File("rb"),
|
|
116
|
+
default="-",
|
|
117
|
+
help=INPUT_FILE_HELP,
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"-t",
|
|
121
|
+
"--trim",
|
|
122
|
+
is_flag=True,
|
|
123
|
+
help="Trim trailing newlines from input (helpful on Windows where `\\r\\n` is appended to echoed text)",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"-p",
|
|
127
|
+
"--prefix",
|
|
128
|
+
is_flag=True,
|
|
129
|
+
help="Include an integer literal prefix on the output (e.g. 0b for binary)",
|
|
130
|
+
)
|
|
131
|
+
@click.option(
|
|
132
|
+
"-s",
|
|
133
|
+
"--separator",
|
|
134
|
+
type=click.STRING,
|
|
135
|
+
default=" ",
|
|
136
|
+
help=SEPARATOR_HELP,
|
|
137
|
+
)
|
|
138
|
+
@click.option(
|
|
139
|
+
"-w",
|
|
140
|
+
"--word-length",
|
|
141
|
+
"word_length",
|
|
142
|
+
type=click.INT,
|
|
143
|
+
help="Defaults: 8 for binary, 2 for hex, 3 for octal",
|
|
144
|
+
)
|
|
145
|
+
@click.option(
|
|
146
|
+
"-o",
|
|
147
|
+
"--output",
|
|
148
|
+
type=click.File("wb"),
|
|
149
|
+
default="-",
|
|
150
|
+
help=OUTPUT_FILE_HELP,
|
|
151
|
+
)
|
|
152
|
+
def encode(
|
|
153
|
+
encoding: Encoding,
|
|
154
|
+
input: IO[bytes],
|
|
155
|
+
trim: bool,
|
|
156
|
+
prefix: bool,
|
|
157
|
+
separator: str,
|
|
158
|
+
word_length: int | None,
|
|
159
|
+
output: IO[bytes],
|
|
160
|
+
) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Encode binary input as a string
|
|
163
|
+
|
|
164
|
+
\b
|
|
165
|
+
Notes:
|
|
166
|
+
-w/--word-length splits output into N-digit words (default 8 for binary, 2 for hex, 3 for octal)
|
|
167
|
+
If -s/--separator is empty, words are concatenated with no delimiter
|
|
168
|
+
-p/--prefix prepends the prefix to each word individually, unless an empty string is passed as the separator, in which case the entire output is treated as one word and the prefix is prepended once at the beginning.
|
|
169
|
+
Last word may be shorter than word-length if data isn't a round multiple.
|
|
170
|
+
Separator and word length are ignored for base64, as base64 is generated as one continuous string.
|
|
171
|
+
Octal data is padded to [word-length]-digit words unless -s/--separator is empty.
|
|
172
|
+
"""
|
|
173
|
+
# TODO: Figure out how to use complete_one here...
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
c = ENCODINGS[encoding]
|
|
177
|
+
except KeyError:
|
|
178
|
+
click.echo(INVALID_ENCODING_MSG, file=sys.stderr)
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
word_length = word_length if word_length is not None else c.default_word_length
|
|
181
|
+
encode_func = c.encode_func
|
|
182
|
+
|
|
183
|
+
output.write(
|
|
184
|
+
encode_func(
|
|
185
|
+
input.read().rstrip(b"\r\n"), prefix, separator, word_length
|
|
186
|
+
).encode()
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@click.command()
|
|
191
|
+
@click.argument(
|
|
192
|
+
"encoding",
|
|
193
|
+
type=click.Choice(
|
|
194
|
+
[e.value for e in Encoding],
|
|
195
|
+
case_sensitive=False,
|
|
196
|
+
),
|
|
197
|
+
default=Encoding.GUESS,
|
|
198
|
+
)
|
|
199
|
+
@click.option("-i", "--input", type=click.File("rb"), default="-", help=INPUT_FILE_HELP)
|
|
200
|
+
@click.option("-s", "--separator", type=click.STRING, default=" ", help=SEPARATOR_HELP)
|
|
201
|
+
@click.option(
|
|
202
|
+
"-o", "--output", type=click.File("wb"), default="-", help=OUTPUT_FILE_HELP
|
|
203
|
+
)
|
|
204
|
+
def decode(
|
|
205
|
+
encoding: Encoding, input: IO[bytes], separator: str, output: IO[bytes]
|
|
206
|
+
) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Given an encoded string as input, convert to binary data
|
|
209
|
+
|
|
210
|
+
\b
|
|
211
|
+
Notes:
|
|
212
|
+
If input data type is unknown, don't specify an encoding (or use `guess`, the default) to automatically determine what encoding was used.
|
|
213
|
+
Because binary, hex, and base64/32/16 share characters, `guess` will misidentify ambiguous input (e.g. hex containing only 1s and 0s). Always prefer explicitly stating an encoding when possible, especially if the results from `guess` don't make sense.
|
|
214
|
+
If data can't be decoded, the original input will be output instead.
|
|
215
|
+
The separator is automatically removed from the input data, but if it is not found it serves as a noop. However, if words are individually prefixed, the
|
|
216
|
+
separator must be specified in order to detect and remove word prefixes.
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
decode_func = ENCODINGS[encoding].decode_func
|
|
220
|
+
except KeyError:
|
|
221
|
+
click.echo(INVALID_ENCODING_MSG, file=sys.stdout)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
|
|
224
|
+
input_data = input.read()
|
|
225
|
+
result = decode_func(input_data.decode().strip(), separator)
|
|
226
|
+
|
|
227
|
+
if result is not None:
|
|
228
|
+
output.write(result)
|
|
229
|
+
else:
|
|
230
|
+
click.echo(
|
|
231
|
+
"[ERROR] Failed to decode text. Mirroring input instead.", file=sys.stderr
|
|
232
|
+
)
|
|
233
|
+
output.write(input_data)
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flagonfly tools for changing data codec.
|
|
3
|
+
|
|
4
|
+
Supports encoding and decoding of:
|
|
5
|
+
- binary
|
|
6
|
+
- hex
|
|
7
|
+
- base64/32/16 (in `guess` decoding only)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import binascii
|
|
12
|
+
from typing import Callable, TypeAlias
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def binary_encode(
|
|
16
|
+
data: bytes, prefix: bool = False, separator: str = " ", word_length: int = 8
|
|
17
|
+
) -> str:
|
|
18
|
+
# Get all bits as a string of 1s and 0s
|
|
19
|
+
bits: str = "".join(f"{byte:08b}" for byte in data)
|
|
20
|
+
# Chunk into words and separate by separator
|
|
21
|
+
# Only prefix every word if prefixing is enabled and there is a separator
|
|
22
|
+
word_prefix = "0b" if prefix and separator != "" else ""
|
|
23
|
+
words = [
|
|
24
|
+
word_prefix + bits[i : i + word_length]
|
|
25
|
+
for i in range(0, len(bits), word_length)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Only prefix entire string if prefixing is enabled and there is no separator
|
|
29
|
+
prefix_str = "0b" if prefix and separator == "" else ""
|
|
30
|
+
return prefix_str + separator.join(words)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def binary_decode(data: str, separator: str = " ") -> bytes | None:
|
|
34
|
+
"""Decode a string of 1s and 0s, optionally prefixed with 0b, optionally separated by `separator`. If the string doesn't
|
|
35
|
+
contain `separator`, it will be ignored."""
|
|
36
|
+
if separator:
|
|
37
|
+
words = [w.removeprefix("0b") for w in data.split(separator)]
|
|
38
|
+
data = "".join(words)
|
|
39
|
+
else:
|
|
40
|
+
data = data.removeprefix("0b")
|
|
41
|
+
# Convert each byte to an int
|
|
42
|
+
try:
|
|
43
|
+
byte_list = [int(data[i : i + 8], 2) for i in range(0, len(data), 8)]
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Convert list of ints to a bytes object
|
|
48
|
+
return bytes(byte_list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hex_encode(
|
|
52
|
+
data: bytes, prefix: bool = False, separator: str = " ", word_length: int = 2
|
|
53
|
+
) -> str:
|
|
54
|
+
hex_str = data.hex()
|
|
55
|
+
word_prefix = "0x" if prefix and separator != "" else ""
|
|
56
|
+
|
|
57
|
+
words = [
|
|
58
|
+
word_prefix + hex_str[i : i + word_length]
|
|
59
|
+
for i in range(0, len(hex_str), word_length)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Only prefix entire string if prefixing is enabled and there is no separator
|
|
63
|
+
prefix_str = "0x" if prefix and separator == "" else ""
|
|
64
|
+
return prefix_str + separator.join(words)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def hex_decode(data: str, separator: str = " ") -> bytes | None:
|
|
68
|
+
"""Decode a string of hexadecimal data, optionally prefixed with 0x, optionally separated by `separator`. If the string doesn't
|
|
69
|
+
contain `separator`, it will be ignored."""
|
|
70
|
+
if separator:
|
|
71
|
+
words = [w.removeprefix("0x") for w in data.split(separator)]
|
|
72
|
+
data = "".join(words)
|
|
73
|
+
else:
|
|
74
|
+
data = data.removeprefix("0x")
|
|
75
|
+
# Remove separator
|
|
76
|
+
data = data.replace(separator, "")
|
|
77
|
+
# Convert each byte to an int
|
|
78
|
+
# Convert list of ints to a bytes object
|
|
79
|
+
try:
|
|
80
|
+
return bytes.fromhex(data)
|
|
81
|
+
except ValueError:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
DecodeFunc: TypeAlias = Callable[[str, str], bytes | None]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_B64_DECODE_FUNCS = (
|
|
89
|
+
base64.b16decode,
|
|
90
|
+
base64.b32decode,
|
|
91
|
+
base64.b64decode,
|
|
92
|
+
base64.urlsafe_b64decode,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_guess_b64_decoded_data(data_with_separator_removed: str) -> bytes | None:
|
|
97
|
+
for f in _B64_DECODE_FUNCS:
|
|
98
|
+
try:
|
|
99
|
+
data = f(data_with_separator_removed)
|
|
100
|
+
return data
|
|
101
|
+
except binascii.Error:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_GUESS_DECODE_FUNCS = (
|
|
108
|
+
binary_decode,
|
|
109
|
+
hex_decode,
|
|
110
|
+
lambda data, _sep: get_guess_b64_decoded_data(data),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def guess_decode(data: str, separator: str = " ") -> bytes | None:
|
|
115
|
+
for func in _GUESS_DECODE_FUNCS:
|
|
116
|
+
if (d := func(data, separator)) is not None:
|
|
117
|
+
return d
|
|
118
|
+
|
|
119
|
+
return None
|