diff --git a/docs/schema_doc.md b/docs/schema_doc.md index d089039f7..dcd6445cd 100644 --- a/docs/schema_doc.md +++ b/docs/schema_doc.md @@ -505,41 +505,22 @@ How to handle reads from the file ##### `pseudofiles..read.` Read a zero -###### `pseudofiles..read..model` Read modelling method (read a zero) - -||| -|-|-| -|__Type__|`"zero"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"zero"`||Read modelling method (read a zero)| ##### `pseudofiles..read.` Read empty file -###### `pseudofiles..read..model` Read modelling method (read empty file) - -||| -|-|-| -|__Type__|`"empty"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"empty"`||Read modelling method (read empty file)| ##### `pseudofiles..read.` Read a constant buffer -###### `pseudofiles..read..model` Read modelling method (read a constant buffer) - -||| -|-|-| -|__Type__|`"const_buf"`| - - -###### `pseudofiles..read..val` Constant buffer - -||| -|-|-| -|__Type__|string| - -The string with the contents of the pseudofile - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"const_buf"`||Read modelling method (read a constant buffer)| +|`val`|string||Pseudofile contents| ##### `pseudofiles..read.` Read a constant map @@ -626,55 +607,24 @@ When this is a list of integers, it treated as a byte array. When this is a list ##### `pseudofiles..read.` Read from a host file -###### `pseudofiles..read..model` Read modelling method (read from a host file) - -||| -|-|-| -|__Type__|`"from_file"`| - - -###### `pseudofiles..read..filename` Path to host file - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"from_file"`||Read modelling method (read from a host file)| +|`filename`|string||Path to host file| ##### `pseudofiles..read.` Read from a custom PyPlugin -###### `pseudofiles..read..model` Read modelling method (read from a custom pyplugin) - -||| -|-|-| -|__Type__|`"from_plugin"`| - - -###### `pseudofiles..read..plugin` Name of the loaded PyPlugin - -||| -|-|-| -|__Type__|string| - - -###### `pseudofiles..read..function` Function to call - -||| -|-|-| -|__Type__|string or null| -|__Default__|`read`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"from_plugin"`||Read modelling method (read from a custom pyplugin)| +|`plugin`|string||Name of the loaded PyPlugin| +|`function`|string or null|`read`|Function to call| ##### `pseudofiles..read.` Default -###### `pseudofiles..read..model` Read modelling method (default) - -||| -|-|-| -|__Type__|`"default"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"default"`||Read modelling method (default)| #### `pseudofiles..write` Write ||| @@ -686,65 +636,30 @@ How to handle writes to the file ##### `pseudofiles..write.` Write to host file -###### `pseudofiles..write..model` Write modelling method (write to host file) - -||| -|-|-| -|__Type__|`"to_file"`| - - -###### `pseudofiles..write..filename` Path to host file - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"to_file"`||Write modelling method (write to host file)| +|`filename`|string||Path to host file| ##### `pseudofiles..write.` Read from a custom PyPlugin -###### `pseudofiles..write..model` Write modelling method (read from a custom pyplugin) - -||| -|-|-| -|__Type__|`"from_plugin"`| - - -###### `pseudofiles..write..plugin` Name of the loaded PyPlugin - -||| -|-|-| -|__Type__|string| - - -###### `pseudofiles..write..function` Function to call - -||| -|-|-| -|__Type__|string or null| -|__Default__|`write`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"from_plugin"`||Write modelling method (read from a custom pyplugin)| +|`plugin`|string||Name of the loaded PyPlugin| +|`function`|string or null|`write`|Function to call| ##### `pseudofiles..write.` Discard write -###### `pseudofiles..write..model` Write modelling method (discard write) - -||| -|-|-| -|__Type__|`"discard"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"discard"`||Write modelling method (discard write)| ##### `pseudofiles..write.` Default -###### `pseudofiles..write..model` Write modelling method (default) - -||| -|-|-| -|__Type__|`"default"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"default"`||Write modelling method (default)| #### `pseudofiles..ioctl` ioctl ||| @@ -783,55 +698,24 @@ plugin: my_plugin ###### `pseudofiles..ioctl..` Return a constant -###### `pseudofiles..ioctl...model` ioctl modelling method (return a constant) - -||| -|-|-| -|__Type__|`"return_const"`| - - -###### `pseudofiles..ioctl...val` Constant to return - -||| -|-|-| -|__Type__|integer| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"return_const"`||ioctl modelling method (return a constant)| +|`val`|integer||Constant to return| ###### `pseudofiles..ioctl..` Symbolic execution -###### `pseudofiles..ioctl...model` ioctl modelling method (symbolic execution) - -||| -|-|-| -|__Type__|`"symex"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"symex"`||ioctl modelling method (symbolic execution)| ###### `pseudofiles..ioctl..` ioctl from a custom PyPlugin -###### `pseudofiles..ioctl...model` ioctl modelling method (ioctl from a custom pyplugin) - -||| -|-|-| -|__Type__|`"from_plugin"`| - - -###### `pseudofiles..ioctl...plugin` Name of the loaded PyPlugin - -||| -|-|-| -|__Type__|string| - - -###### `pseudofiles..ioctl...function` Function to call - -||| -|-|-| -|__Type__|string or null| -|__Default__|`ioctl`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`model`|`"from_plugin"`||ioctl modelling method (ioctl from a custom pyplugin)| +|`plugin`|string||Name of the loaded PyPlugin| +|`function`|string or null|`ioctl`|Function to call| ## `nvram` NVRAM ||| @@ -981,289 +865,88 @@ Files to create in the guest filesystem Add a file with contents specified inline in this config -##### `static_files...type` Type of file action (add inline file) - -||| -|-|-| -|__Type__|`"inline_file"`| - - -##### `static_files...mode` Permissions of file - -||| -|-|-| -|__Type__|integer| - - -##### `static_files...contents` Contents of file - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"inline_file"`||Type of file action (add inline file)| +|`mode`|integer||Permissions of file| +|`contents`|string||Contents of file| #### `static_files..` Copy host file Copy a file from the host into the guest -##### `static_files...type` Type of file action (copy host file) - -||| -|-|-| -|__Type__|`"host_file"`| - - -##### `static_files...mode` Permissions of file - -||| -|-|-| -|__Type__|integer| - - -##### `static_files...host_path` Host path - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"host_file"`||Type of file action (copy host file)| +|`mode`|integer||Permissions of file| +|`host_path`|string||Host path| #### `static_files..` Add directory -##### `static_files...type` Type of file action (add directory) - -||| -|-|-| -|__Type__|`"dir"`| - - -##### `static_files...mode` Permissions of directory - -||| -|-|-| -|__Type__|integer| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"dir"`||Type of file action (add directory)| +|`mode`|integer||Permissions of directory| #### `static_files..` Add symbolic link -##### `static_files...type` Type of file action (add symbolic link) - -||| -|-|-| -|__Type__|`"symlink"`| - - -##### `static_files...target` Target linked path - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"symlink"`||Type of file action (add symbolic link)| +|`target`|string||Target linked path| #### `static_files..` Add device file -##### `static_files...type` Type of file action (add device file) - -||| -|-|-| -|__Type__|`"dev"`| - - -##### `static_files...devtype` Type of device file - -||| -|-|-| -|__Type__|`"char"` or `"block"`| - - -##### `static_files...major` Major device number - -||| -|-|-| -|__Type__|integer| - - -##### `static_files...minor` Minor device number - -||| -|-|-| -|__Type__|integer| - - -##### `static_files...mode` Permissions of device file - -||| -|-|-| -|__Type__|integer| - - -```yaml -438 -``` - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"dev"`||Type of file action (add device file)| +|`devtype`|`"char"` or `"block"`||Type of device file| +|`major`|integer||Major device number| +|`minor`|integer||Minor device number| +|`mode`|integer||Permissions of device file| #### `static_files..` Delete file -##### `static_files...type` Type of file action (delete file) - -||| -|-|-| -|__Type__|`"delete"`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"delete"`||Type of file action (delete file)| #### `static_files..` Move file -##### `static_files...type` Type of file action (move file) - -||| -|-|-| -|__Type__|`"move"`| - - -##### `static_files...from` File to be moved to the specified location - -||| -|-|-| -|__Type__|string| - - -##### `static_files...mode` Permissions of target file - -||| -|-|-| -|__Type__|integer or null| -|__Default__|`null`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"move"`||Type of file action (move file)| +|`from`|string||File to be moved to the specified location| +|`mode`|integer or null|`null`|Permissions of target file| #### `static_files..` Shim file -##### `static_files...type` Type of file action (shim file) - -||| -|-|-| -|__Type__|`"shim"`| - - -##### `static_files...target` Target file we want the shim to be symlinked to - -||| -|-|-| -|__Type__|string| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`type`|`"shim"`||Type of file action (shim file)| +|`target`|string||Target file we want the shim to be symlinked to| #### `static_files..` Patch binary file +Make a patch to a binary file at the specified offset. This can either be arbitrary bytes specified as a hex string, or assembly code that will be automatically assembled in the specified mode. -##### `static_files...type` Type of file action (patch binary file) - -||| -|-|-| -|__Type__|`"binary_patch"`| - - -##### `static_files...file_offset` File offset (integer) - -||| -|-|-| -|__Type__|integer| -|__Default__|`null`| - -Offset in the file to patch, from the start of the file - -##### `static_files...hex_bytes` Bytes to write (hex string) - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - -Hex string of bytes to write at the offset - -```yaml -DEADBEEF -``` - -```yaml -90 90 -``` - -##### `static_files...asm` Assembly code to write (runs through keystone) - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - -Assembly code to write at the offset. This will be assembled and written to the file. - -```yaml -nop -``` - -```yaml -'mov r0, #0xdeadbeef' -``` - -##### `static_files...mode` Assembly mode - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - -What mode to use for assembly with asm. - -```yaml -arm -``` - -```yaml -thumb -``` - +|Field|Type|Default|Title|Examples| +|-|-|-|-|-| +|`type`|`"binary_patch"`||Type of file action (patch binary file)|| +|`file_offset`|integer||File offset (integer)|| +|`hex_bytes`|string or null|`null`|Bytes to write at offset (hex string)|`DEADBEEF`, `90 90`| +|`asm`|string or null|`null`|Assembly code to write at offset (runs through keystone)|`nop`, `'mov r0, #0xdeadbeef'`| +|`mode`|string or null|`null`|Assembly mode|`arm`, `thumb`| ## `plugins` Plugins ### `plugins.` Plugin -#### `plugins..description` Plugin description - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - - -#### `plugins..depends_on` Plugin dependency - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - - -#### `plugins..enabled` Enable plugin - -||| -|-|-| -|__Type__|boolean| -|__Default__|`true`| - -Whether to enable this plugin (default depends on plugin) - -#### `plugins..version` Plugin version - -||| -|-|-| -|__Type__|string or null| -|__Default__|`null`| - - +|Field|Type|Default|Title| +|-|-|-|-| +|`description`|string or null|`null`|Plugin description| +|`depends_on`|string or null|`null`|Plugin dependency| +|`enabled`|boolean|`true`|Enable this plugin (default depends on plugin)| +|`version`|string or null|`null`|Plugin version| ## `network` Network Configuration ||| diff --git a/src/penguin/penguin_config/gen_docs.py b/src/penguin/penguin_config/gen_docs.py index 7dff7ed01..522a906bf 100644 --- a/src/penguin/penguin_config/gen_docs.py +++ b/src/penguin/penguin_config/gen_docs.py @@ -12,6 +12,17 @@ import structure +def type_has_simple_name(ty): + """ + Determine whether a type is a regular Python type and not a Pydantic model class. + """ + try: + gen_docs_type_name(ty) + return True + except ValueError: + return False + + def gen_docs_yaml_dump(x): """ Convert x to YAML for use in generated docs. @@ -93,6 +104,26 @@ def gen_docs_field(path, docs_field, include_type=True): return out +def gen_docs_compact_field_table(fields): + """ + For fields that do not have any nested structure and only simple types, + generate a compact table to make the docs easier to read. + """ + + has_examples = any(field.examples for field in fields.values()) + out = f"|Field|Type|Default|Title|{'Examples|' if has_examples else ''}\n" + out += f"|-|-|-|-|{'-|' if has_examples else ''}\n" + for name, field in fields.items(): + field = DocsField.from_field(field) + # Ensure there is no extra information for this field that doesn't fit in the compact table + assert not field.merge_behavior and not field.description, (name, field) + type_name = gen_docs_type_name(field.type_) + default = "" if field.default is PydanticUndefined else "`" + gen_docs_yaml_dump(field.default) + "`" + examples = ", ".join(f"`{gen_docs_yaml_dump(example)}`" for example in field.examples) + out += f"|`{name}`|{type_name}|{default}|{field.title}|{examples + '|' if has_examples else ''}\n" + return out + + @dataclasses.dataclass(frozen=True) class DocsField: """Information about a field of the config, for generating docs""" @@ -223,12 +254,23 @@ def gen_docs(path=[], docs_field=DocsField.from_type(structure.Main)): elif is_model: # The type inherits `BaseModel` but not `RootModel` + # Render high-level info before specific sub-fields out += gen_docs_field(path=path, docs_field=docs_field, include_type=False) - for name, info in type_.model_fields.items(): - out += gen_docs( - path=path + [name], - docs_field=DocsField.from_field(info), - ) + + all_fields_compact = all( + type_has_simple_name(field.annotation) and not field.description + for field in type_.model_fields.values() + ) + if all_fields_compact: + # We can render this as one compact table since there is no recursive structure here + out += gen_docs_compact_field_table(type_.model_fields) + else: + # Recursively render docs for each field + for name, info in type_.model_fields.items(): + out += gen_docs( + path=path + [name], + docs_field=DocsField.from_field(info), + ) elif type_origin is dict: # The type is `dict[T, U]`. diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index e73c20785..1453a91fc 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -376,10 +376,7 @@ class Core(PartialModelMixin, BaseModel): ( "val", str, - Field( - title="Constant buffer", - description="The string with the contents of the pseudofile", - ), + Field(title="Pseudofile contents"), ), ), ), @@ -697,7 +694,7 @@ class LibInject(PartialModelMixin, BaseModel): ( "mode", int, - Field(title="Permissions of device file", examples=[0o666]), + Field(title="Permissions of device file"), ), ), ), @@ -735,24 +732,19 @@ class LibInject(PartialModelMixin, BaseModel): dict( discrim_val="binary_patch", title="Patch binary file", - description=None, + description="Make a patch to a binary file at the specified offset. This can either be arbitrary bytes specified as a hex string, or assembly code that will be automatically assembled in the specified mode.", fields=( ( "file_offset", int, - Field( - default=None, - title="File offset (integer)", - description="Offset in the file to patch, from the start of the file" - ), + Field(title="File offset (integer)"), ), ( "hex_bytes", Optional[str], Field( default=None, - title="Bytes to write (hex string)", - description="Hex string of bytes to write at the offset", + title="Bytes to write at offset (hex string)", examples=["DEADBEEF", "90 90"], ), ), @@ -761,8 +753,7 @@ class LibInject(PartialModelMixin, BaseModel): Optional[str], Field( default=None, - title="Assembly code to write (runs through keystone)", - description="Assembly code to write at the offset. This will be assembled and written to the file.", + title="Assembly code to write at offset (runs through keystone)", examples=["nop", "mov r0, #0xdeadbeef"], ), ), @@ -772,7 +763,6 @@ class LibInject(PartialModelMixin, BaseModel): Field( default=None, title="Assembly mode", - description="What mode to use for assembly with asm.", examples=["arm", "thumb"], ), ), @@ -824,11 +814,7 @@ class Plugin(PartialModelMixin, BaseModel): depends_on: Annotated[Optional[str], Field(None, title="Plugin dependency")] enabled: Annotated[ bool, - Field( - True, - title="Enable plugin", - description="Whether to enable this plugin (default depends on plugin)", - ), + Field(True, title="Enable this plugin (default depends on plugin)"), ] version: Annotated[Optional[str], Field(None, title="Plugin version")]