Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,46 @@ offset size field

An invalid name such as `1x` produces `invalid --struct "1x"`, a name not defined in the file produces `unknown struct "Nope" in --struct`, and `--struct` with `suggest` or `check` produces `suggest does not take --struct` or `check does not take --struct`. All of these exit with status 2.

`--format json` is available only with `layout`; `--format=json` is also supported and `--format text` is the default. It prints the same structs as the text listing (also with `--struct`) as one JSON array, indented by two spaces, with the keys in the order shown below. `fields` lists the fields with the offset and size of each one and `gaps` lists every run of padding bytes, including the tail, so the sizes in `gaps` add up to `padding`. For `struct Pair { u8 a; u32 b; }`:

```sh
structpad layout --format json pair.sp
```

```text
[
{
"name": "Pair",
"size": 8,
"align": 4,
"padding": 3,
"packed": false,
"fields": [
{
"name": "a",
"type": "u8",
"offset": 0,
"size": 1
},
{
"name": "b",
"type": "u32",
"offset": 4,
"size": 4
}
],
"gaps": [
{
"offset": 1,
"size": 3
}
]
}
]
```

An invalid value such as `xml` produces `invalid --format "xml" (use text or json)`, and `--format` with `suggest` or `check` produces `suggest does not take --format` or `check does not take --format`. Both exit with status 2.

Select the ABI with `--abi lp64` (the default) or `--abi ilp32`; for example:

```sh
Expand Down
30 changes: 24 additions & 6 deletions src/structpad/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from structpad.layout import calculate_layout
from structpad.model import InputError
from structpad.parser import parse
from structpad.render import render_check, render_layout, render_suggest
from structpad.render import render_check, render_layout, render_layout_json, render_suggest

_USAGE = (
"usage: structpad layout [--abi lp64|ilp32] [--struct NAME]... FILE\n"
"usage: structpad layout [--abi lp64|ilp32] [--struct NAME]... [--format text|json] FILE\n"
" structpad suggest [--abi lp64|ilp32] FILE\n"
" structpad check [--abi lp64|ilp32] [--max-padding N] [--ignore NAME]... FILE\n"
)
Expand All @@ -29,8 +29,9 @@ def _write_error(stderr: BinaryIO, message: str) -> None:

def _parse_options(
command: str, arguments: list[str]
) -> tuple[str, int, str, tuple[str, ...], tuple[str, ...]] | str:
) -> tuple[str, int, str, tuple[str, ...], tuple[str, ...], str] | str:
abi = "lp64"
output_format = "text"
max_padding = 0
ignored: list[str] = []
selected: list[str] = []
Expand All @@ -57,6 +58,22 @@ def _parse_options(
index += 1
continue

if argument == "--format" or argument.startswith("--format="):
if command != "layout":
return f"{command} does not take --format"
if argument == "--format":
if index + 1 >= len(arguments):
return "--format needs a value"
value = arguments[index + 1]
index += 2
else:
value = argument[len("--format=") :]
index += 1
if value not in {"text", "json"}:
return f'invalid --format "{_escape_controls(value)}" (use text or json)'
output_format = value
continue

if argument == "--max-padding" or argument.startswith("--max-padding="):
if command != "check":
return f"{command} does not take --max-padding"
Expand Down Expand Up @@ -114,7 +131,7 @@ def _parse_options(

if file_name is None:
return "missing FILE"
return abi, max_padding, file_name, tuple(ignored), tuple(selected)
return abi, max_padding, file_name, tuple(ignored), tuple(selected), output_format


def run(args: list[str], stdin: BinaryIO, stdout: BinaryIO, stderr: BinaryIO) -> int:
Expand All @@ -135,7 +152,7 @@ def run(args: list[str], stdin: BinaryIO, stdout: BinaryIO, stderr: BinaryIO) ->
if isinstance(parsed_options, str):
_write_error(stderr, parsed_options)
return 2
abi, max_padding, file_name, ignored_names, selected_names = parsed_options
abi, max_padding, file_name, ignored_names, selected_names, output_format = parsed_options
display = "<stdin>" if file_name == "-" else file_name.replace("\\", "/")

try:
Expand Down Expand Up @@ -174,7 +191,8 @@ def run(args: list[str], stdin: BinaryIO, stdout: BinaryIO, stderr: BinaryIO) ->
ignored = frozenset(ignored_names)

if command == "layout":
stdout.write(render_layout(structs, abi, selected_names or None))
renderer = render_layout_json if output_format == "json" else render_layout
stdout.write(renderer(structs, abi, selected_names or None))
return 0
if command == "suggest":
stdout.write(render_suggest(structs, abi))
Expand Down
47 changes: 47 additions & 0 deletions src/structpad/render.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Byte-oriented text renderers for structpad commands."""

import json
from collections.abc import Mapping, Sequence

from structpad.layout import StructLayout, calculate_layout
Expand Down Expand Up @@ -59,6 +60,52 @@ def render_layout(
return (("\n\n".join(blocks) + "\n") if blocks else "").encode("utf-8")


def render_layout_json(
structs: Sequence[Struct], abi: str, selected_names: Sequence[str] | None = None
) -> bytes:
"""Render layouts as a deterministic JSON array of UTF-8 bytes."""
definitions = _definitions(structs)
selected = None if selected_names is None else frozenset(selected_names)
layouts: list[dict[str, object]] = []

for struct in structs:
if selected is not None and struct.name not in selected:
continue

layout = calculate_layout(struct, abi, definitions)
fields = [
{
"name": field_layout.field.name,
"type": _field_type(field_layout.field),
"offset": field_layout.offset,
"size": field_layout.size,
}
for field_layout in layout.field_layouts
]
gaps: list[dict[str, int]] = []
offset = 0
for field_layout in layout.field_layouts:
if field_layout.offset > offset:
gaps.append({"offset": offset, "size": field_layout.offset - offset})
offset = field_layout.offset + field_layout.size
if offset < layout.size:
gaps.append({"offset": offset, "size": layout.size - offset})

layouts.append(
{
"name": struct.name,
"size": layout.size,
"align": layout.alignment,
"padding": layout.padding,
"packed": struct.packed,
"fields": fields,
"gaps": gaps,
}
)

return (json.dumps(layouts, indent=2) + "\n").encode("utf-8")


def render_suggest(structs: Sequence[Struct], abi: str) -> bytes:
"""Render stable field-order suggestions as UTF-8 bytes with LF endings."""
definitions = _definitions(structs)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
run = importlib.import_module("structpad.cli").run

_USAGE = (
b"usage: structpad layout [--abi lp64|ilp32] [--struct NAME]... FILE\n"
b"usage: structpad layout [--abi lp64|ilp32] [--struct NAME]... [--format text|json] FILE\n"
b" structpad suggest [--abi lp64|ilp32] FILE\n"
b" structpad check [--abi lp64|ilp32] [--max-padding N] [--ignore NAME]... FILE\n"
)
Expand Down
Loading
Loading