"""
Rockwell Allen-Bradley ControlLogix PLC logic parsing methods.
Authors
- Craig Buchanan
- Christopher Goes
"""
from peat import log
from .clx_cip import ClxCIP
from .clx_const import *
from .clx_relay_ladder_parser import decompile_ladder_process_logic
from .clx_string_logic_parser import (
DisassembleStringLogicError,
decompile_string_process_logic,
disassemble_string_process_logic,
)
LayoutType = list[tuple[int, str]]
AttrsType = dict[int, dict]
MapAttrsType = dict[int, dict[int, int | bytes]]
[docs]
def parse_logic(logic_dict: dict[str, dict], driver: ClxCIP | None = None) -> str:
"""
Parse memory layout, symbol list, and logic from a attributes :class:`dict`.
The logic dict should have the following keys
- ``template_attributes``
- ``template_tags``
- ``symbol_attributes``
- ``program_attributes``
- ``program_symbol_attributes``
- ``program_routine_attributes``
- ``program_routine_tags``
- ``map_attributes``
- ``map_cxn_attributes``
Args:
logic_dict: Attributes dict
driver: CIP driver to use for tag lookups, if needed
Returns:
The parsed memory layout, symbol list, and logic as a humand-readable
string, or an empty string if the parse failed
"""
log.debug("Parsing memory layout, symbol list, and logic from dict")
if not logic_dict:
log.error("Empty logic dictionary was passed to parse_logic")
return ""
logic_str = ""
try:
memory_list = extract_memory_layout(
symbol_attributes=logic_dict["symbol_attributes"],
program_attributes=logic_dict["program_attributes"],
program_routine_attributes=logic_dict["program_routine_attributes"],
program_symbol_attributes=logic_dict["program_symbol_attributes"],
map_attributes=logic_dict["map_attributes"],
map_cxn_attributes=logic_dict["map_cxn_attributes"],
)
log.debug("Extracted memory layout")
logic_str += memory_layout_to_str(memory_list)
# Symbol list is needed for Relay Ladder sections of program
symbol_list = extract_symbol_list(
symbol_attributes=logic_dict["symbol_attributes"],
program_symbol_attributes=logic_dict["program_symbol_attributes"],
program_attributes=logic_dict["program_attributes"],
)
log.debug("Extracted symbol list")
parsed_logic = parse_process_logic(
program_routine_tags=logic_dict["program_routine_tags"],
program_routine_attributes=logic_dict["program_routine_attributes"],
symbol_list=symbol_list,
template_tags=logic_dict["template_tags"],
driver=driver,
)
log.debug("Finished logic parsing")
logic_str += parsed_logic
except Exception:
log.exception("Unknown exception occurred during logic parsing")
return logic_str
log.debug("Finished parsing memory layout, symbol list, and logic")
return logic_str
[docs]
def memory_layout_to_str(memory_list: LayoutType) -> str:
"""
Parses a memory list into a formatted layout of the memory.
Args:
memory_list: List of Value/Label tuples defining the memory layout
Returns:
Formatted human-readable layout of the memory.
"""
if not memory_list:
return ""
layout_str = "Memory Layout:"
for sym_val, sym_str in sorted(memory_list):
layout_str += f"\n[0x{sym_val & 0xFFFFFFFF:0>8x}] "
layout_str += sym_str
if isinstance(sym_str, bytes):
log.warning("memory_layout_to_str: unexpected bytes!")
return layout_str
[docs]
def parse_process_logic(
program_routine_tags: AttrsType,
program_routine_attributes: AttrsType,
symbol_list: LayoutType,
template_tags: AttrsType,
driver: ClxCIP | None = None,
) -> str:
# TODO: docstring
log.info("Decompiling Process Logic...")
logic_str = ""
for program_id, program_tag in program_routine_tags.items():
for routine_id, logic_data in program_tag.items():
# TODO: function to parse routine and/or program
logic_str += f"\nProgram: {hex(program_id)}"
logic_str += f"\nRoutine: {hex(routine_id)}"
disassembly = ""
decompiled = ""
attrs = program_routine_attributes[program_id][routine_id]
routine_language = attrs[0x01] # Routine language (IEC 61131-3)
starting_address = attrs[0x02] # Starting address of the routine
# TODO
# Save the logic_data (the raw binary) for each routine?
# Use: print_bytes_line(logic_data)
# TODO: function to parse each language type
lang_name = LOGIC_LANGUAGE_BY_INT.get(routine_language, f"unknown {routine_language}")
log.debug(f"Language for routine {routine_id}: {lang_name} (program id: {program_id})")
if routine_language == LANG_RLL:
logic_str += "\nProgram is Relay Ladder Logic\n"
# TODO: finish implementing RLL disassembly and save it somewhere
# disassembly = disassemble_ladder_process_logic(
# process_logic=logic_data, starting_address=starting_address
# )
decompiled = decompile_ladder_process_logic(
process_logic=logic_data,
symbol_list=symbol_list,
template_tags=template_tags,
starting_address=starting_address,
)
# TODO: add logic samples from L8 to unit tests
elif routine_language in [LANG_STL, LANG_SFC, LANG_FBD]:
# TODO: write the routine language type?
try:
disassembly, logic_string = disassemble_string_process_logic(
logic_bytes=logic_data, logic_language=routine_language
)
# TODO: get attributes lookup for resolve_token_name
decompiled = decompile_string_process_logic(
logic_string=logic_string,
template_tags=template_tags,
driver=driver,
)
except DisassembleStringLogicError as e:
log.warning(repr(e))
else:
disassembly = "ERROR: Language not recognized"
if disassembly:
log.trace(f"Size of disassmebly for '{lang_name}': {len(disassembly)}")
logic_str += "\nDisassembly:"
logic_str += disassembly
else:
log.warning(
f"No disassembled '{lang_name}' logic for routine {routine_id} "
f"and program {program_id}"
)
if decompiled:
log.trace(f"Size of decompilation for '{lang_name}': {len(decompiled)}")
logic_str += "\nDecompiled:"
logic_str += decompiled
else:
log.warning(
f"No decompiled '{lang_name}' logic for routine {routine_id} "
f"and program {program_id}"
)
return logic_str
# NOTE(cegoes, 12/09/2024): leave this alone, it's been needed
# more than once for random, silly things.
# def print_bytes_line(msg) -> str:
# out = ""
# for ch in msg:
# out += "{:0>2x}".format(ch)
# return out