pktfmt 0.1.0__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.
- pktfmt-0.1.0/PKG-INFO +81 -0
- pktfmt-0.1.0/README.md +61 -0
- pktfmt-0.1.0/pktfmt/__init__.py +3 -0
- pktfmt-0.1.0/pktfmt/__main__.py +6 -0
- pktfmt-0.1.0/pktfmt/cli.py +87 -0
- pktfmt-0.1.0/pktfmt/parser.py +142 -0
- pktfmt-0.1.0/pktfmt/renderer.py +158 -0
- pktfmt-0.1.0/pktfmt.egg-info/PKG-INFO +81 -0
- pktfmt-0.1.0/pktfmt.egg-info/SOURCES.txt +12 -0
- pktfmt-0.1.0/pktfmt.egg-info/dependency_links.txt +1 -0
- pktfmt-0.1.0/pktfmt.egg-info/entry_points.txt +2 -0
- pktfmt-0.1.0/pktfmt.egg-info/top_level.txt +2 -0
- pktfmt-0.1.0/pyproject.toml +31 -0
- pktfmt-0.1.0/setup.cfg +4 -0
pktfmt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pktfmt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate RFC-style ASCII packet diagrams from field definitions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: packet,diagram,ascii,rfc,network,protocol
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
17
|
+
Classifier: Topic :: System :: Networking
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# pktfmt
|
|
22
|
+
|
|
23
|
+
Generate RFC-style ASCII packet diagrams from field definitions.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pktfmt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Inline format
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pktfmt "Type:16,Length:16,Payload:*"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
```
|
|
41
|
+
0 1 2 3
|
|
42
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
43
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
44
|
+
| Type | Length |
|
|
45
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
46
|
+
: Payload :
|
|
47
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### JSON file
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pktfmt packet.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"name": "MyPacket",
|
|
59
|
+
"fields": [
|
|
60
|
+
{"name": "Type", "bits": 16},
|
|
61
|
+
{"name": "Length", "bits": 16},
|
|
62
|
+
{"name": "Payload", "bits": "*"}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Options
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pktfmt "Type:16,Data:32" --bits-per-row 16 # Custom row width
|
|
71
|
+
pktfmt "Type:32" --no-ruler # Omit bit number header
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Field syntax
|
|
75
|
+
|
|
76
|
+
- `Name:N` - Fixed-width field of N bits
|
|
77
|
+
- `Name:*` - Variable-length field (rendered with `:` borders)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
pktfmt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# pktfmt
|
|
2
|
+
|
|
3
|
+
Generate RFC-style ASCII packet diagrams from field definitions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pktfmt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Inline format
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pktfmt "Type:16,Length:16,Payload:*"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
```
|
|
21
|
+
0 1 2 3
|
|
22
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
23
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
24
|
+
| Type | Length |
|
|
25
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
26
|
+
: Payload :
|
|
27
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### JSON file
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pktfmt packet.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"name": "MyPacket",
|
|
39
|
+
"fields": [
|
|
40
|
+
{"name": "Type", "bits": 16},
|
|
41
|
+
{"name": "Length", "bits": 16},
|
|
42
|
+
{"name": "Payload", "bits": "*"}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pktfmt "Type:16,Data:32" --bits-per-row 16 # Custom row width
|
|
51
|
+
pktfmt "Type:32" --no-ruler # Omit bit number header
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Field syntax
|
|
55
|
+
|
|
56
|
+
- `Name:N` - Fixed-width field of N bits
|
|
57
|
+
- `Name:*` - Variable-length field (rendered with `:` borders)
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Command-line interface for pktfmt."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .parser import parse_input
|
|
9
|
+
from .renderer import render_diagram
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
13
|
+
"""Create the argument parser."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="pktfmt",
|
|
16
|
+
description="Generate RFC-style ASCII packet diagrams from field definitions.",
|
|
17
|
+
epilog="""
|
|
18
|
+
Examples:
|
|
19
|
+
pktfmt "Type:16,Length:16,Payload:*"
|
|
20
|
+
pktfmt packet.json
|
|
21
|
+
pktfmt "Type:16,Data:32" --bits-per-row 16
|
|
22
|
+
""",
|
|
23
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"input",
|
|
28
|
+
help="Field definition (inline 'Name:bits,...' format) or path to JSON file",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-b", "--bits-per-row",
|
|
33
|
+
type=int,
|
|
34
|
+
default=32,
|
|
35
|
+
metavar="N",
|
|
36
|
+
help="Number of bits per row (default: 32)",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--no-ruler",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Omit the bit number header",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"-v", "--version",
|
|
47
|
+
action="version",
|
|
48
|
+
version=f"%(prog)s {__version__}",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
55
|
+
"""Main entry point for the CLI.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
argv: Command-line arguments (defaults to sys.argv[1:])
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Exit code (0 for success, non-zero for errors)
|
|
62
|
+
"""
|
|
63
|
+
parser = create_parser()
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
fields = parse_input(args.input)
|
|
68
|
+
diagram = render_diagram(
|
|
69
|
+
fields,
|
|
70
|
+
bits_per_row=args.bits_per_row,
|
|
71
|
+
show_ruler=not args.no_ruler,
|
|
72
|
+
)
|
|
73
|
+
print(diagram)
|
|
74
|
+
return 0
|
|
75
|
+
except FileNotFoundError as e:
|
|
76
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
sys.exit(main())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Parse packet field definitions from inline strings and JSON files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Field:
|
|
9
|
+
"""Represents a packet field with a name and bit width."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str, bits: Union[int, str]):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.bits = bits # int for fixed width, "*" for variable
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def is_variable(self) -> bool:
|
|
17
|
+
return self.bits == "*"
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"Field({self.name!r}, {self.bits!r})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_inline(definition: str) -> List[Field]:
|
|
24
|
+
"""Parse inline format: 'Name:bits,Name:bits,Name:*'
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
definition: Comma-separated field definitions
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of Field objects
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the format is invalid
|
|
34
|
+
"""
|
|
35
|
+
fields = []
|
|
36
|
+
|
|
37
|
+
for part in definition.split(","):
|
|
38
|
+
part = part.strip()
|
|
39
|
+
if not part:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if ":" not in part:
|
|
43
|
+
raise ValueError(f"Invalid field format: '{part}' (expected 'Name:bits')")
|
|
44
|
+
|
|
45
|
+
name, bits_str = part.rsplit(":", 1)
|
|
46
|
+
name = name.strip()
|
|
47
|
+
bits_str = bits_str.strip()
|
|
48
|
+
|
|
49
|
+
if not name:
|
|
50
|
+
raise ValueError(f"Field name cannot be empty: '{part}'")
|
|
51
|
+
|
|
52
|
+
if bits_str == "*":
|
|
53
|
+
bits = "*"
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
bits = int(bits_str)
|
|
57
|
+
if bits <= 0:
|
|
58
|
+
raise ValueError(f"Bit width must be positive: '{part}'")
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise ValueError(f"Invalid bit width: '{bits_str}' in '{part}'")
|
|
61
|
+
|
|
62
|
+
fields.append(Field(name, bits))
|
|
63
|
+
|
|
64
|
+
if not fields:
|
|
65
|
+
raise ValueError("No fields defined")
|
|
66
|
+
|
|
67
|
+
return fields
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_json(json_input: Union[str, Path]) -> List[Field]:
|
|
71
|
+
"""Parse JSON format for packet definition.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
json_input: JSON string or path to JSON file
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of Field objects
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If the JSON format is invalid
|
|
81
|
+
"""
|
|
82
|
+
# Try to load as file first
|
|
83
|
+
if isinstance(json_input, Path) or (isinstance(json_input, str) and not json_input.strip().startswith("{")):
|
|
84
|
+
path = Path(json_input)
|
|
85
|
+
if path.exists():
|
|
86
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
87
|
+
data = json.load(f)
|
|
88
|
+
else:
|
|
89
|
+
raise FileNotFoundError(f"File not found: {json_input}")
|
|
90
|
+
else:
|
|
91
|
+
data = json.loads(json_input)
|
|
92
|
+
|
|
93
|
+
if not isinstance(data, dict):
|
|
94
|
+
raise ValueError("JSON must be an object with a 'fields' array")
|
|
95
|
+
|
|
96
|
+
if "fields" not in data:
|
|
97
|
+
raise ValueError("JSON must contain a 'fields' array")
|
|
98
|
+
|
|
99
|
+
fields = []
|
|
100
|
+
for i, field_data in enumerate(data["fields"]):
|
|
101
|
+
if not isinstance(field_data, dict):
|
|
102
|
+
raise ValueError(f"Field {i} must be an object")
|
|
103
|
+
|
|
104
|
+
if "name" not in field_data:
|
|
105
|
+
raise ValueError(f"Field {i} missing 'name'")
|
|
106
|
+
|
|
107
|
+
if "bits" not in field_data:
|
|
108
|
+
raise ValueError(f"Field {i} missing 'bits'")
|
|
109
|
+
|
|
110
|
+
name = field_data["name"]
|
|
111
|
+
bits = field_data["bits"]
|
|
112
|
+
|
|
113
|
+
if bits != "*" and not isinstance(bits, int):
|
|
114
|
+
raise ValueError(f"Field '{name}': bits must be an integer or '*'")
|
|
115
|
+
|
|
116
|
+
if isinstance(bits, int) and bits <= 0:
|
|
117
|
+
raise ValueError(f"Field '{name}': bit width must be positive")
|
|
118
|
+
|
|
119
|
+
fields.append(Field(name, bits))
|
|
120
|
+
|
|
121
|
+
if not fields:
|
|
122
|
+
raise ValueError("No fields defined")
|
|
123
|
+
|
|
124
|
+
return fields
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_input(input_str: str) -> List[Field]:
|
|
128
|
+
"""Auto-detect and parse input format (inline or JSON file).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
input_str: Either an inline definition or a path to a JSON file
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of Field objects
|
|
135
|
+
"""
|
|
136
|
+
# Check if it's a file path
|
|
137
|
+
path = Path(input_str)
|
|
138
|
+
if path.exists() and path.suffix.lower() == ".json":
|
|
139
|
+
return parse_json(path)
|
|
140
|
+
|
|
141
|
+
# Otherwise treat as inline format
|
|
142
|
+
return parse_inline(input_str)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Render ASCII packet diagrams in RFC style."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from .parser import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def render_diagram(fields: List[Field], bits_per_row: int = 32, show_ruler: bool = True) -> str:
|
|
8
|
+
"""Render fields as an RFC-style ASCII packet diagram.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
fields: List of Field objects to render
|
|
12
|
+
bits_per_row: Number of bits per row (default 32)
|
|
13
|
+
show_ruler: Whether to show the bit number header
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
ASCII diagram string
|
|
17
|
+
"""
|
|
18
|
+
lines = []
|
|
19
|
+
|
|
20
|
+
# Generate ruler
|
|
21
|
+
if show_ruler:
|
|
22
|
+
lines.extend(_generate_ruler(bits_per_row))
|
|
23
|
+
|
|
24
|
+
# Build list of rows, each row is list of (name, width, is_variable, is_continuation)
|
|
25
|
+
rows = []
|
|
26
|
+
current_row = []
|
|
27
|
+
current_bit = 0
|
|
28
|
+
|
|
29
|
+
for field in fields:
|
|
30
|
+
if field.is_variable:
|
|
31
|
+
# Flush current row if any
|
|
32
|
+
if current_row:
|
|
33
|
+
rows.append(current_row)
|
|
34
|
+
current_row = []
|
|
35
|
+
current_bit = 0
|
|
36
|
+
|
|
37
|
+
# Variable field gets its own full-width row
|
|
38
|
+
rows.append([(field.name, bits_per_row, True, False)])
|
|
39
|
+
else:
|
|
40
|
+
bits_remaining = field.bits
|
|
41
|
+
is_first_part = True
|
|
42
|
+
|
|
43
|
+
while bits_remaining > 0:
|
|
44
|
+
space_in_row = bits_per_row - current_bit
|
|
45
|
+
|
|
46
|
+
if bits_remaining <= space_in_row:
|
|
47
|
+
# Field fits in current row
|
|
48
|
+
current_row.append((field.name, bits_remaining, False, not is_first_part))
|
|
49
|
+
current_bit += bits_remaining
|
|
50
|
+
bits_remaining = 0
|
|
51
|
+
|
|
52
|
+
# If row is complete, flush it
|
|
53
|
+
if current_bit == bits_per_row:
|
|
54
|
+
rows.append(current_row)
|
|
55
|
+
current_row = []
|
|
56
|
+
current_bit = 0
|
|
57
|
+
else:
|
|
58
|
+
# Field spans to next row
|
|
59
|
+
current_row.append((field.name, space_in_row, False, not is_first_part))
|
|
60
|
+
rows.append(current_row)
|
|
61
|
+
current_row = []
|
|
62
|
+
current_bit = 0
|
|
63
|
+
bits_remaining -= space_in_row
|
|
64
|
+
is_first_part = False
|
|
65
|
+
|
|
66
|
+
# Flush any remaining partial row
|
|
67
|
+
if current_row:
|
|
68
|
+
rows.append(current_row)
|
|
69
|
+
|
|
70
|
+
# Render rows
|
|
71
|
+
separator = "+" + "-+" * bits_per_row
|
|
72
|
+
prev_continues_to_next = False
|
|
73
|
+
|
|
74
|
+
for i, row in enumerate(rows):
|
|
75
|
+
row_width = sum(f[1] for f in row)
|
|
76
|
+
is_full_row = row_width == bits_per_row
|
|
77
|
+
has_variable = any(f[2] for f in row)
|
|
78
|
+
|
|
79
|
+
# Check if first field in this row is a continuation
|
|
80
|
+
first_is_continuation = row[0][3] if row else False
|
|
81
|
+
|
|
82
|
+
# Check if last field continues to next row
|
|
83
|
+
continues_to_next = False
|
|
84
|
+
if i + 1 < len(rows) and row:
|
|
85
|
+
next_row = rows[i + 1]
|
|
86
|
+
if next_row and next_row[0][3]: # Next row's first field is continuation
|
|
87
|
+
continues_to_next = True
|
|
88
|
+
|
|
89
|
+
# Add separator (skip if this row continues from previous)
|
|
90
|
+
if not first_is_continuation:
|
|
91
|
+
if is_full_row:
|
|
92
|
+
lines.append(separator)
|
|
93
|
+
else:
|
|
94
|
+
lines.append("+" + "-+" * row_width)
|
|
95
|
+
# Render the row content
|
|
96
|
+
lines.append(_render_field_row(row, bits_per_row, has_variable))
|
|
97
|
+
else:
|
|
98
|
+
# Continuation row - just side borders, blank inside (no separator, no content row)
|
|
99
|
+
lines.append("|" + " " * (bits_per_row * 2 - 1) + "|")
|
|
100
|
+
|
|
101
|
+
prev_continues_to_next = continues_to_next
|
|
102
|
+
|
|
103
|
+
# Final separator
|
|
104
|
+
if rows:
|
|
105
|
+
last_row = rows[-1]
|
|
106
|
+
last_width = sum(f[1] for f in last_row)
|
|
107
|
+
lines.append("+" + "-+" * last_width)
|
|
108
|
+
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _generate_ruler(bits_per_row: int) -> List[str]:
|
|
113
|
+
"""Generate the bit number ruler header."""
|
|
114
|
+
# First line: byte markers (0, 1, 2, 3 for 32-bit)
|
|
115
|
+
byte_line = " "
|
|
116
|
+
for byte_num in range(bits_per_row // 8):
|
|
117
|
+
byte_line += f"{byte_num:<19}"
|
|
118
|
+
byte_line = byte_line[:bits_per_row * 2]
|
|
119
|
+
|
|
120
|
+
# Second line: bit numbers within each byte (0-9, repeating)
|
|
121
|
+
bit_line = " "
|
|
122
|
+
for i in range(bits_per_row):
|
|
123
|
+
bit_line += f"{i % 10} "
|
|
124
|
+
bit_line = bit_line.rstrip()
|
|
125
|
+
|
|
126
|
+
return [byte_line.rstrip(), bit_line]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _render_field_row(row_fields: List[tuple], bits_per_row: int, has_variable: bool) -> str:
|
|
130
|
+
"""Render a single row of fields.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
row_fields: List of (name, width, is_variable, is_continuation) tuples
|
|
134
|
+
bits_per_row: Number of bits per row
|
|
135
|
+
has_variable: Whether this row contains a variable-length field
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Rendered row string
|
|
139
|
+
"""
|
|
140
|
+
border = ":" if has_variable else "|"
|
|
141
|
+
result = border
|
|
142
|
+
|
|
143
|
+
for name, width, is_var, is_continuation in row_fields:
|
|
144
|
+
cell_width = width * 2 - 1
|
|
145
|
+
|
|
146
|
+
if is_continuation:
|
|
147
|
+
# Show blank or continuation indicator for multi-row fields
|
|
148
|
+
display_name = " " * cell_width
|
|
149
|
+
else:
|
|
150
|
+
# Center the name
|
|
151
|
+
if len(name) > cell_width:
|
|
152
|
+
display_name = name[:cell_width]
|
|
153
|
+
else:
|
|
154
|
+
display_name = name.center(cell_width)
|
|
155
|
+
|
|
156
|
+
result += display_name + border
|
|
157
|
+
|
|
158
|
+
return result
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pktfmt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate RFC-style ASCII packet diagrams from field definitions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: packet,diagram,ascii,rfc,network,protocol
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
17
|
+
Classifier: Topic :: System :: Networking
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# pktfmt
|
|
22
|
+
|
|
23
|
+
Generate RFC-style ASCII packet diagrams from field definitions.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pktfmt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Inline format
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pktfmt "Type:16,Length:16,Payload:*"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
```
|
|
41
|
+
0 1 2 3
|
|
42
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
43
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
44
|
+
| Type | Length |
|
|
45
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
46
|
+
: Payload :
|
|
47
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### JSON file
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pktfmt packet.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"name": "MyPacket",
|
|
59
|
+
"fields": [
|
|
60
|
+
{"name": "Type", "bits": 16},
|
|
61
|
+
{"name": "Length", "bits": 16},
|
|
62
|
+
{"name": "Payload", "bits": "*"}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Options
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pktfmt "Type:16,Data:32" --bits-per-row 16 # Custom row width
|
|
71
|
+
pktfmt "Type:32" --no-ruler # Omit bit number header
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Field syntax
|
|
75
|
+
|
|
76
|
+
- `Name:N` - Fixed-width field of N bits
|
|
77
|
+
- `Name:*` - Variable-length field (rendered with `:` borders)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pktfmt/__init__.py
|
|
4
|
+
pktfmt/__main__.py
|
|
5
|
+
pktfmt/cli.py
|
|
6
|
+
pktfmt/parser.py
|
|
7
|
+
pktfmt/renderer.py
|
|
8
|
+
pktfmt.egg-info/PKG-INFO
|
|
9
|
+
pktfmt.egg-info/SOURCES.txt
|
|
10
|
+
pktfmt.egg-info/dependency_links.txt
|
|
11
|
+
pktfmt.egg-info/entry_points.txt
|
|
12
|
+
pktfmt.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pktfmt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generate RFC-style ASCII packet diagrams from field definitions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["packet", "diagram", "ascii", "rfc", "network", "protocol"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Documentation",
|
|
24
|
+
"Topic :: System :: Networking",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
pktfmt = "pktfmt.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
pktfmt-0.1.0/setup.cfg
ADDED