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 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,3 @@
1
+ """pktfmt - Generate RFC-style ASCII packet diagrams."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m pktfmt."""
2
+
3
+ from pktfmt.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ pktfmt = pktfmt.cli:main
@@ -0,0 +1,2 @@
1
+ dist
2
+ pktfmt
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+