c2c3c52274
The AbCip controller browse could never be verified end to end: libplctag's ab_server answers CIP service 0x55 (Get Instance Attribute List) on the Symbol Object class with "unsupported", so the `@tags` walk had no backend. #468's own definition of done offered a sim that serves `@tags` as an alternative to real hardware; this is that. The fixture now applies a vendored patch teaching ab_server to serve the Symbol Object listing, with partial-transfer paging. Correctness is checked against libplctag's own `list_tags_logix` rather than against our decoder — our decoder is the thing under test, so its agreement would prove nothing. The clone is also pinned to a concrete tag: the Dockerfile called `--branch release` "pinned" while tracking a branch that moves. That immediately exposed a real defect. CipSymbolObjectDecoder advanced to an even byte boundary after each symbol name, but entries are packed with no padding — libplctag walks them by sizeof(entry) + string_len, and a captured reply confirms it. Any odd-length name therefore desynchronised every entry behind it, and odd-length names are ordinary. In the live browse below, `Motor_Run` is 9 bytes and first on the wire, so the old decoder would have shown that one tag and silently dropped the other eight. Every existing multi-entry test passed throughout, because the test builder padded exactly as the decoder did: the two agreed with each other and neither agreed with a controller. The builder no longer pads, and the layout is pinned by a captured reply (CipSymbolObjectWireCaptureTests) instead of by a second statement of the assumption. Restoring the alignment step reddens exactly the three new tests and none of the 29 others. Live gate on docker-dev, all three of #468's criteria: - populated tree — all nine seeded tags render as leaves, in wire order - refresh re-captures - leaf commit — Pump_1_Speed + TestDINT saved with their CIP tag paths The gate also surfaced two things it was not asked about: browse-commit drops the discovered data type, so a browsed REAL commits as a DINT and the driver decodes it as one (filed as #536), and Test Connect fails against this fixture regardless of the device because it probes `@raw_cpu_type` (documented). TwinCAT and FOCAS still have no browsable backend; #468's AbCip half is closed, theirs is not. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
205 lines
10 KiB
Diff
205 lines
10 KiB
Diff
diff --git a/src/tools/ab_server/cip.c b/src/tools/ab_server/cip.c
|
|
index 68a5794..f266bf6 100644
|
|
--- a/src/tools/ab_server/cip.c
|
|
+++ b/src/tools/ab_server/cip.c
|
|
@@ -111,6 +111,18 @@ const uint8_t CIP_OBJ_CONNECTION_MANAGER[] = {0x20, 0x06, 0x24, 0x01};
|
|
|
|
#define CIP_TAG_MAX_INDEXES ((uint32_t)3) /* should double check for OMRON */
|
|
|
|
+/* Symbol Object (class 0x6B) listing — see handle_list_tags_request(). */
|
|
+#define CIP_SYMBOL_OBJECT_CLASS ((uint8_t)0x6b)
|
|
+#define CIP_LIST_TAGS_PATH_SIZE ((size_t)6) /* 0x20 0x6B 0x25 0x00 <instance u16> */
|
|
+#define CIP_LIST_TAGS_ENTRY_HEADER_SIZE ((size_t)22)
|
|
+#define CIP_LIST_TAGS_PAYLOAD_MIN_SIZE ((size_t)2) /* two bytes for the attribute count */
|
|
+#define CIP_LIST_TAGS_ATTR_COUNT ((uint16_t)4)
|
|
+#define CIP_SYM_ATTR_NAME ((uint16_t)0x01)
|
|
+#define CIP_SYM_ATTR_TYPE ((uint16_t)0x02)
|
|
+#define CIP_SYM_ATTR_ELEM_SIZE ((uint16_t)0x07)
|
|
+#define CIP_SYM_ATTR_DIMENSIONS ((uint16_t)0x08)
|
|
+#define CIP_SYM_TYPE_DIM_SHIFT (13) /* bits 13-14 carry the array dimension count. */
|
|
+
|
|
#define MAX_SUB_PACKETS ((uint16_t)1000) /* maximum number of sub-packets in a multi-service request */
|
|
|
|
#define CIP_MINIMAL_RESPONSE_SIZE ((size_t)6) /* four bytes for header plus 2 for optional extended status. */
|
|
@@ -126,6 +138,8 @@ static slice_s handle_read_request(uint8_t cip_service, slice_s cip_service_path
|
|
plc_s *plc);
|
|
static slice_s handle_write_request(uint8_t cip_service, slice_s cip_service_path, slice_s cip_service_payload, slice_s output,
|
|
plc_s *plc);
|
|
+static slice_s handle_list_tags_request(uint8_t cip_service, slice_s cip_service_path, slice_s cip_service_payload,
|
|
+ slice_s output, plc_s *plc);
|
|
static slice_s handle_multi_request(uint8_t cip_service, slice_s cip_service_path, slice_s cip_service_payload, slice_s output,
|
|
plc_s *plc);
|
|
|
|
@@ -220,6 +234,10 @@ slice_s cip_dispatch_request(slice_s input, slice_s output, plc_s *plc) {
|
|
return handle_write_request(cip_service, cip_service_path, cip_service_payload, output, plc);
|
|
break;
|
|
|
|
+ case CIP_SRV_INSTANCES_ATTRIBS:
|
|
+ return handle_list_tags_request(cip_service, cip_service_path, cip_service_payload, output, plc);
|
|
+ break;
|
|
+
|
|
default: return make_cip_log_error(output, cip_service, CIP_ERR_UNSUPPORTED, false, 0); break;
|
|
}
|
|
}
|
|
@@ -877,6 +895,160 @@ slice_s handle_read_request(uint8_t cip_service, slice_s cip_service_path, slice
|
|
}
|
|
|
|
|
|
+/*
|
|
+ * Handle the CIP "Get Instance Attribute List" service (0x55) against the Symbol
|
|
+ * Object class (0x6B). This is the request a client issues to enumerate the
|
|
+ * controller's symbol table — libplctag sends it when an application reads the
|
|
+ * "@tags" pseudo-tag.
|
|
+ *
|
|
+ * Request path (6 bytes):
|
|
+ * 0x20 0x6B 8-bit class segment, Symbol Object
|
|
+ * 0x25 0x00 16-bit instance segment + pad
|
|
+ * u16 instance id to start from
|
|
+ *
|
|
+ * Request payload:
|
|
+ * u16 attribute count
|
|
+ * u16 * count attribute ids
|
|
+ *
|
|
+ * The reply layout is determined by the requested attributes, so this server
|
|
+ * serves exactly the set libplctag asks for — symbol type, element size, array
|
|
+ * dimensions, symbol name — and refuses anything else rather than returning a
|
|
+ * differently-shaped reply the client would silently misparse.
|
|
+ *
|
|
+ * Reply payload is a packed run of entries, each:
|
|
+ * u32 instance id
|
|
+ * u16 symbol type
|
|
+ * u16 element length in bytes
|
|
+ * u32 [3] array dimensions
|
|
+ * u16 name length
|
|
+ * u8 * length name
|
|
+ *
|
|
+ * NOTE: entries are NOT padded. The next entry starts on the byte immediately
|
|
+ * after the last name byte, even when that leaves it on an odd offset — see
|
|
+ * libplctag's own reply walker in eip_cip_special.c, which advances by
|
|
+ * sizeof(entry) + string_len with no alignment step.
|
|
+ *
|
|
+ * When the entries do not all fit in the response buffer the reply carries
|
|
+ * status 0x06 (partial transfer) and the client asks again starting from the
|
|
+ * last instance id it saw, plus one.
|
|
+ */
|
|
+slice_s handle_list_tags_request(uint8_t cip_service, slice_s cip_service_path, slice_s cip_service_payload, slice_s output,
|
|
+ plc_s *plc) {
|
|
+ uint16_t start_instance_id = 0;
|
|
+ uint16_t attribute_count = 0;
|
|
+ uint16_t instance_id = 0;
|
|
+ size_t payload_offset = 0;
|
|
+ size_t output_offset = CIP_RESPONSE_HEADER_SIZE;
|
|
+ bool more_to_come = false;
|
|
+ tag_def_s *tag = NULL;
|
|
+
|
|
+ log_info("Processing Get Instance Attribute List (Symbol Object) request.");
|
|
+
|
|
+ /* Only the controller-scope listing is served. A program-scope listing
|
|
+ * arrives with a leading symbolic segment naming the program, and this
|
|
+ * server has no notion of program scopes to list. */
|
|
+ if(slice_len(cip_service_path) != CIP_LIST_TAGS_PATH_SIZE || slice_get_uint8(cip_service_path, 0) != 0x20
|
|
+ || slice_get_uint8(cip_service_path, 1) != CIP_SYMBOL_OBJECT_CLASS || slice_get_uint8(cip_service_path, 2) != 0x25
|
|
+ || slice_get_uint8(cip_service_path, 3) != 0x00) {
|
|
+ log_info("Unsupported path for a Symbol Object listing request!");
|
|
+ log_info_slice(cip_service_path);
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_PATH_SEGMENT, false, 0);
|
|
+ }
|
|
+
|
|
+ start_instance_id = slice_get_uint16_le(cip_service_path, 4);
|
|
+
|
|
+ log_info("Listing symbols from instance %u.", (unsigned int)start_instance_id);
|
|
+
|
|
+ if(slice_len(cip_service_payload) < CIP_LIST_TAGS_PAYLOAD_MIN_SIZE) {
|
|
+ log_info("Insufficient data in the Symbol Object listing payload!");
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_INSUFFICIENT_DATA, false, 0);
|
|
+ }
|
|
+
|
|
+ attribute_count = slice_get_uint16_le(cip_service_payload, payload_offset);
|
|
+ payload_offset += 2;
|
|
+
|
|
+ if(slice_len(cip_service_payload) < payload_offset + ((size_t)attribute_count * 2)) {
|
|
+ log_info("Symbol Object listing payload is shorter than its attribute count claims!");
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_INSUFFICIENT_DATA, false, 0);
|
|
+ }
|
|
+
|
|
+ if(attribute_count != CIP_LIST_TAGS_ATTR_COUNT
|
|
+ || slice_get_uint16_le(cip_service_payload, payload_offset + 0) != CIP_SYM_ATTR_TYPE
|
|
+ || slice_get_uint16_le(cip_service_payload, payload_offset + 2) != CIP_SYM_ATTR_ELEM_SIZE
|
|
+ || slice_get_uint16_le(cip_service_payload, payload_offset + 4) != CIP_SYM_ATTR_DIMENSIONS
|
|
+ || slice_get_uint16_le(cip_service_payload, payload_offset + 6) != CIP_SYM_ATTR_NAME) {
|
|
+ log_info("Symbol Object listing requested an attribute set this server does not serve!");
|
|
+ log_info_slice(cip_service_payload);
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_INVALID_PARAM, false, 0);
|
|
+ }
|
|
+
|
|
+ if(slice_len(output) < CIP_RESPONSE_HEADER_SIZE) {
|
|
+ log_info("Insufficient space in the output buffer for the response header!");
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_INSUFFICIENT_DATA, false, 0);
|
|
+ }
|
|
+
|
|
+ /* Instance ids are assigned by position in the tag list. The walk order is
|
|
+ * stable across requests, so ids ascend the same way every time and paging
|
|
+ * can simply resume at the first id the client has not seen. */
|
|
+ for(tag = plc->tags; tag; tag = tag->next_tag) {
|
|
+ size_t name_len = 0;
|
|
+ size_t entry_len = 0;
|
|
+ uint16_t symbol_type = 0;
|
|
+
|
|
+ instance_id++;
|
|
+
|
|
+ if(instance_id < start_instance_id) { continue; }
|
|
+
|
|
+ name_len = strlen(tag->name);
|
|
+ entry_len = CIP_LIST_TAGS_ENTRY_HEADER_SIZE + name_len;
|
|
+
|
|
+ if(output_offset + entry_len > slice_len(output)) {
|
|
+ log_info("Response buffer full — %u symbol(s) sent, more to come.", (unsigned int)(instance_id - 1));
|
|
+ more_to_come = true;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ /* Bits 13-14 of the symbol type carry the array dimension count; the
|
|
+ * low 12 bits carry the CIP type code. */
|
|
+ symbol_type = (uint16_t)(tag->tag_type | ((uint16_t)(tag->num_dimensions & 0x03) << CIP_SYM_TYPE_DIM_SHIFT));
|
|
+
|
|
+ slice_set_uint32_le(output, output_offset + 0, (uint32_t)instance_id);
|
|
+ slice_set_uint16_le(output, output_offset + 4, symbol_type);
|
|
+ slice_set_uint16_le(output, output_offset + 6, (uint16_t)tag->elem_size);
|
|
+
|
|
+ for(size_t dim = 0; dim < 3; dim++) {
|
|
+ uint32_t dim_size = (dim < tag->num_dimensions) ? (uint32_t)tag->dimensions[dim] : 0;
|
|
+ slice_set_uint32_le(output, output_offset + 8 + (dim * 4), dim_size);
|
|
+ }
|
|
+
|
|
+ slice_set_uint16_le(output, output_offset + 20, (uint16_t)name_len);
|
|
+
|
|
+ if(!slice_copy_data_in(slice_from_slice(output, output_offset + CIP_LIST_TAGS_ENTRY_HEADER_SIZE, name_len),
|
|
+ (uint8_t *)tag->name, name_len)) {
|
|
+ log_info("Unable to copy the symbol name into the response!");
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_INVALID_PARAM, false, 0);
|
|
+ }
|
|
+
|
|
+ output_offset += entry_len;
|
|
+ }
|
|
+
|
|
+ /* A partial reply carrying no entries at all would leave the client asking
|
|
+ * for the same instance forever, because it advances its cursor from the
|
|
+ * last entry it received. Fail the request instead of looping it. */
|
|
+ if(more_to_come && output_offset == CIP_RESPONSE_HEADER_SIZE) {
|
|
+ log_info("A single symbol entry does not fit in the response buffer!");
|
|
+ return make_cip_log_error(output, cip_service, CIP_ERR_EXTENDED, true, CIP_ERR_EX_TOO_LONG);
|
|
+ }
|
|
+
|
|
+ slice_set_uint8(output, 0, cip_service | CIP_DONE);
|
|
+ slice_set_uint8(output, 1, 0); /* reserved */
|
|
+ slice_set_uint8(output, 2, (more_to_come ? CIP_ERR_FRAG : CIP_OK)); /* status */
|
|
+ slice_set_uint8(output, 3, 0); /* no extended error */
|
|
+
|
|
+ return slice_from_slice(output, 0, output_offset);
|
|
+}
|
|
+
|
|
+
|
|
#define CIP_WRITE_MIN_SIZE (6)
|
|
#define CIP_WRITE_FRAG_MIN_SIZE (10)
|
|
|