busctl: split out introspection parser from tree logic so that we can reuse it for a future "busctl introspect" command
This commit is contained in:
parent
a4962513c5
commit
a1ad376761
|
@ -2898,7 +2898,9 @@ test_resolve_CFLAGS = \
|
|||
-pthread
|
||||
|
||||
busctl_SOURCES = \
|
||||
src/libsystemd/sd-bus/busctl.c
|
||||
src/libsystemd/sd-bus/busctl.c \
|
||||
src/libsystemd/sd-bus/busctl-introspect.c \
|
||||
src/libsystemd/sd-bus/busctl-introspect.h
|
||||
|
||||
busctl_LDADD = \
|
||||
libsystemd-dump.la \
|
||||
|
|
655
src/libsystemd/sd-bus/busctl-introspect.c
Normal file
655
src/libsystemd/sd-bus/busctl-introspect.c
Normal file
|
@ -0,0 +1,655 @@
|
|||
/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
|
||||
|
||||
/***
|
||||
This file is part of systemd.
|
||||
|
||||
Copyright 2014 Lennart Poettering
|
||||
|
||||
systemd is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
systemd is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with systemd; If not, see <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#include "util.h"
|
||||
#include "xml.h"
|
||||
#include "sd-bus-vtable.h"
|
||||
|
||||
#include "busctl-introspect.h"
|
||||
|
||||
#define NODE_DEPTH_MAX 16
|
||||
|
||||
typedef struct Context {
|
||||
const XMLIntrospectOps *ops;
|
||||
void *userdata;
|
||||
|
||||
char *interface_name;
|
||||
uint64_t interface_flags;
|
||||
|
||||
char *member_name;
|
||||
char *member_signature;
|
||||
char *member_result;
|
||||
uint64_t member_flags;
|
||||
|
||||
const char *current;
|
||||
void *xml_state;
|
||||
} Context;
|
||||
|
||||
static int parse_xml_annotation(Context *context, uint64_t *flags) {
|
||||
|
||||
enum {
|
||||
STATE_ANNOTATION,
|
||||
STATE_NAME,
|
||||
STATE_VALUE
|
||||
} state = STATE_ANNOTATION;
|
||||
|
||||
_cleanup_free_ char *field = NULL, *value = NULL;
|
||||
|
||||
assert(context);
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
|
||||
int t;
|
||||
|
||||
t = xml_tokenize(&context->current, &name, &context->xml_state, NULL);
|
||||
if (t < 0) {
|
||||
log_error("XML parse error.");
|
||||
return t;
|
||||
}
|
||||
|
||||
if (t == XML_END) {
|
||||
log_error("Premature end of XML data.");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
||||
case STATE_ANNOTATION:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_NAME;
|
||||
|
||||
else if (streq_ptr(name, "value"))
|
||||
state = STATE_VALUE;
|
||||
|
||||
else {
|
||||
log_error("Unexpected <annotation> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "annotation"))) {
|
||||
|
||||
if (flags) {
|
||||
if (streq_ptr(field, "org.freedesktop.DBus.Deprecated")) {
|
||||
|
||||
if (streq_ptr(value, "true"))
|
||||
*flags |= SD_BUS_VTABLE_DEPRECATED;
|
||||
|
||||
} else if (streq_ptr(field, "org.freedesktop.DBus.Method.NoReply")) {
|
||||
|
||||
if (streq_ptr(value, "true"))
|
||||
*flags |= SD_BUS_VTABLE_METHOD_NO_REPLY;
|
||||
|
||||
} else if (streq_ptr(field, "org.freedesktop.DBus.Property.EmitsChangedSignal")) {
|
||||
|
||||
if (streq_ptr(value, "const"))
|
||||
*flags |= SD_BUS_VTABLE_PROPERTY_CONST;
|
||||
else if (streq_ptr(value, "invalidates"))
|
||||
*flags |= SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION;
|
||||
else if (streq_ptr(value, "false"))
|
||||
*flags |= SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
} else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <annotation>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE) {
|
||||
free(field);
|
||||
field = name;
|
||||
name = NULL;
|
||||
|
||||
state = STATE_ANNOTATION;
|
||||
} else {
|
||||
log_error("Unexpected token in <annotation>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_VALUE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE) {
|
||||
free(value);
|
||||
value = name;
|
||||
name = NULL;
|
||||
|
||||
state = STATE_ANNOTATION;
|
||||
} else {
|
||||
log_error("Unexpected token in <annotation>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached("Bad state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_xml_node(Context *context, const char *prefix, unsigned n_depth) {
|
||||
|
||||
enum {
|
||||
STATE_NODE,
|
||||
STATE_NODE_NAME,
|
||||
STATE_INTERFACE,
|
||||
STATE_INTERFACE_NAME,
|
||||
STATE_METHOD,
|
||||
STATE_METHOD_NAME,
|
||||
STATE_METHOD_ARG,
|
||||
STATE_METHOD_ARG_NAME,
|
||||
STATE_METHOD_ARG_TYPE,
|
||||
STATE_METHOD_ARG_DIRECTION,
|
||||
STATE_SIGNAL,
|
||||
STATE_SIGNAL_NAME,
|
||||
STATE_SIGNAL_ARG,
|
||||
STATE_SIGNAL_ARG_NAME,
|
||||
STATE_SIGNAL_ARG_TYPE,
|
||||
STATE_PROPERTY,
|
||||
STATE_PROPERTY_NAME,
|
||||
STATE_PROPERTY_TYPE,
|
||||
STATE_PROPERTY_ACCESS,
|
||||
} state = STATE_NODE;
|
||||
|
||||
_cleanup_free_ char *node_path = NULL;
|
||||
const char *np = prefix;
|
||||
int r;
|
||||
|
||||
assert(context);
|
||||
assert(prefix);
|
||||
|
||||
if (n_depth > NODE_DEPTH_MAX) {
|
||||
log_error("<node> depth too high.");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
int t;
|
||||
|
||||
t = xml_tokenize(&context->current, &name, &context->xml_state, NULL);
|
||||
if (t < 0) {
|
||||
log_error("XML parse error.");
|
||||
return t;
|
||||
}
|
||||
|
||||
if (t == XML_END) {
|
||||
log_error("Premature end of XML data.");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
||||
case STATE_NODE:
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_NODE_NAME;
|
||||
else {
|
||||
log_error("Unexpected <node> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
|
||||
if (streq_ptr(name, "interface"))
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (streq_ptr(name, "node")) {
|
||||
|
||||
r = parse_xml_node(context, np, n_depth+1);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <node> tag %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "node"))) {
|
||||
|
||||
if (context->ops->on_path) {
|
||||
r = context->ops->on_path(node_path ? node_path : np, context->userdata);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
} else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <node>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_NODE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE) {
|
||||
|
||||
free(node_path);
|
||||
|
||||
if (name[0] == '/') {
|
||||
node_path = name;
|
||||
name = NULL;
|
||||
} else {
|
||||
|
||||
if (endswith(prefix, "/"))
|
||||
node_path = strappend(prefix, name);
|
||||
else
|
||||
node_path = strjoin(prefix, "/", name, NULL);
|
||||
if (!node_path)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
np = node_path;
|
||||
state = STATE_NODE;
|
||||
} else {
|
||||
log_error("Unexpected token in <node>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_INTERFACE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_INTERFACE_NAME;
|
||||
else {
|
||||
log_error("Unexpected <interface> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "method"))
|
||||
state = STATE_METHOD;
|
||||
else if (streq_ptr(name, "signal"))
|
||||
state = STATE_SIGNAL;
|
||||
else if (streq_ptr(name, "property"))
|
||||
state = STATE_PROPERTY;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, &context->interface_flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <interface> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "interface")))
|
||||
|
||||
state = STATE_NODE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <interface>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_INTERFACE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_INTERFACE;
|
||||
else {
|
||||
log_error("Unexpected token in <interface>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_METHOD_NAME;
|
||||
else {
|
||||
log_error("Unexpected <method> attribute %s", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "arg"))
|
||||
state = STATE_METHOD_ARG;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, &context->member_flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <method> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "method")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <method> (1).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD;
|
||||
else {
|
||||
log_error("Unexpected token in <method> (2).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_METHOD_ARG_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_METHOD_ARG_TYPE;
|
||||
else if (streq_ptr(name, "direction"))
|
||||
state = STATE_METHOD_ARG_DIRECTION;
|
||||
else {
|
||||
log_error("Unexpected method <arg> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected method <arg> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "arg")))
|
||||
|
||||
state = STATE_METHOD;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in method <arg>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_DIRECTION:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (4)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_SIGNAL_NAME;
|
||||
else {
|
||||
log_error("Unexpected <signal> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "arg"))
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, &context->member_flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <signal> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "signal")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <signal>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL;
|
||||
else {
|
||||
log_error("Unexpected token in <signal>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case STATE_SIGNAL_ARG:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_SIGNAL_ARG_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_SIGNAL_ARG_TYPE;
|
||||
else {
|
||||
log_error("Unexpected signal <arg> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected signal <arg> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "arg")))
|
||||
|
||||
state = STATE_SIGNAL;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in signal <arg> (1).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_ARG_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in signal <arg> (2).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_ARG_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in signal <arg> (3).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_PROPERTY_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_PROPERTY_TYPE;
|
||||
else if (streq_ptr(name, "access"))
|
||||
state = STATE_PROPERTY_ACCESS;
|
||||
else {
|
||||
log_error("Unexpected <property> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(context, &context->member_flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <property> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "property")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <property>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_ACCESS:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (4)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int parse_xml_introspect(const char *prefix, const char *xml, const XMLIntrospectOps *ops, void *userdata) {
|
||||
Context context = {
|
||||
.ops = ops,
|
||||
.userdata = userdata,
|
||||
.current = xml,
|
||||
};
|
||||
|
||||
int r;
|
||||
|
||||
assert(prefix);
|
||||
assert(xml);
|
||||
assert(ops);
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
|
||||
r = xml_tokenize(&context.current, &name, &context.xml_state, NULL);
|
||||
if (r < 0) {
|
||||
log_error("XML parse error");
|
||||
return r;
|
||||
}
|
||||
|
||||
if (r == XML_END)
|
||||
break;
|
||||
|
||||
if (r == XML_TAG_OPEN) {
|
||||
|
||||
if (streq(name, "node")) {
|
||||
r = parse_xml_node(&context, prefix, 0);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected tag '%s' in introspection data.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (r != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token.");
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
35
src/libsystemd/sd-bus/busctl-introspect.h
Normal file
35
src/libsystemd/sd-bus/busctl-introspect.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/***
|
||||
This file is part of systemd.
|
||||
|
||||
Copyright 2014 Lennart Poettering
|
||||
|
||||
systemd is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
systemd is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with systemd; If not, see <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
typedef struct XMLIntrospectOps {
|
||||
int (*on_path)(const char *path, void *userdata);
|
||||
int (*on_interface)(const char *name, uint64_t flags);
|
||||
int (*on_method)(const char *name, const char *signature, const char *result, uint64_t flags, void *userdata);
|
||||
int (*on_signal)(const char *name, const char *signature, uint64_t flags, void *userdata);
|
||||
int (*on_property)(const char *name, const char *signature, uint64_t flags);
|
||||
int (*on_writable_property)(const char *name, const char *signature, uint64_t flags);
|
||||
} XMLIntrospectOps;
|
||||
|
||||
int parse_xml_introspect(const char *prefix, const char *xml, const XMLIntrospectOps *ops, void *userdata);
|
|
@ -35,6 +35,7 @@
|
|||
#include "bus-util.h"
|
||||
#include "bus-dump.h"
|
||||
#include "bus-signature.h"
|
||||
#include "busctl-introspect.h"
|
||||
|
||||
static bool arg_no_pager = false;
|
||||
static bool arg_legend = true;
|
||||
|
@ -304,550 +305,27 @@ static void print_tree(const char *prefix, char **l) {
|
|||
print_subtree(prefix, "/", l);
|
||||
}
|
||||
|
||||
static int parse_xml_annotation(
|
||||
const char **p,
|
||||
void **xml_state) {
|
||||
|
||||
|
||||
enum {
|
||||
STATE_ANNOTATION,
|
||||
STATE_NAME,
|
||||
STATE_VALUE
|
||||
} state = STATE_ANNOTATION;
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
|
||||
int t;
|
||||
|
||||
t = xml_tokenize(p, &name, xml_state, NULL);
|
||||
if (t < 0) {
|
||||
log_error("XML parse error.");
|
||||
return t;
|
||||
}
|
||||
|
||||
if (t == XML_END) {
|
||||
log_error("Premature end of XML data.");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
||||
case STATE_ANNOTATION:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_NAME;
|
||||
|
||||
else if (streq_ptr(name, "value"))
|
||||
state = STATE_VALUE;
|
||||
|
||||
else {
|
||||
log_error("Unexpected <annotation> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "annotation")))
|
||||
|
||||
return 0;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <annotation>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_ANNOTATION;
|
||||
else {
|
||||
log_error("Unexpected token in <annotation>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_VALUE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_ANNOTATION;
|
||||
else {
|
||||
log_error("Unexpected token in <annotation>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached("Bad state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_xml_node(
|
||||
const char *prefix,
|
||||
Set *paths,
|
||||
const char **p,
|
||||
void **xml_state) {
|
||||
|
||||
enum {
|
||||
STATE_NODE,
|
||||
STATE_NODE_NAME,
|
||||
STATE_INTERFACE,
|
||||
STATE_INTERFACE_NAME,
|
||||
STATE_METHOD,
|
||||
STATE_METHOD_NAME,
|
||||
STATE_METHOD_ARG,
|
||||
STATE_METHOD_ARG_NAME,
|
||||
STATE_METHOD_ARG_TYPE,
|
||||
STATE_METHOD_ARG_DIRECTION,
|
||||
STATE_SIGNAL,
|
||||
STATE_SIGNAL_NAME,
|
||||
STATE_SIGNAL_ARG,
|
||||
STATE_SIGNAL_ARG_NAME,
|
||||
STATE_SIGNAL_ARG_TYPE,
|
||||
STATE_PROPERTY,
|
||||
STATE_PROPERTY_NAME,
|
||||
STATE_PROPERTY_TYPE,
|
||||
STATE_PROPERTY_ACCESS,
|
||||
} state = STATE_NODE;
|
||||
|
||||
_cleanup_free_ char *node_path = NULL;
|
||||
const char *np = prefix;
|
||||
static int on_path(const char *path, void *userdata) {
|
||||
Set *paths = userdata;
|
||||
int r;
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
int t;
|
||||
assert(paths);
|
||||
|
||||
t = xml_tokenize(p, &name, xml_state, NULL);
|
||||
if (t < 0) {
|
||||
log_error("XML parse error.");
|
||||
return t;
|
||||
}
|
||||
r = set_put_strdup(paths, path);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
if (t == XML_END) {
|
||||
log_error("Premature end of XML data.");
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
||||
case STATE_NODE:
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_NODE_NAME;
|
||||
else {
|
||||
log_error("Unexpected <node> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
|
||||
if (streq_ptr(name, "interface"))
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (streq_ptr(name, "node")) {
|
||||
|
||||
r = parse_xml_node(np, paths, p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <node> tag %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "node"))) {
|
||||
|
||||
if (paths) {
|
||||
if (!node_path) {
|
||||
node_path = strdup(np);
|
||||
if (!node_path)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = set_put(paths, node_path);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
else if (r > 0)
|
||||
node_path = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
} else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <node>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_NODE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE) {
|
||||
|
||||
free(node_path);
|
||||
|
||||
if (name[0] == '/') {
|
||||
node_path = name;
|
||||
name = NULL;
|
||||
} else {
|
||||
|
||||
if (endswith(prefix, "/"))
|
||||
node_path = strappend(prefix, name);
|
||||
else
|
||||
node_path = strjoin(prefix, "/", name, NULL);
|
||||
if (!node_path)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
np = node_path;
|
||||
state = STATE_NODE;
|
||||
} else {
|
||||
log_error("Unexpected token in <node>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_INTERFACE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_INTERFACE_NAME;
|
||||
else {
|
||||
log_error("Unexpected <interface> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "method"))
|
||||
state = STATE_METHOD;
|
||||
else if (streq_ptr(name, "signal"))
|
||||
state = STATE_SIGNAL;
|
||||
else if (streq_ptr(name, "property"))
|
||||
state = STATE_PROPERTY;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <interface> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "interface")))
|
||||
|
||||
state = STATE_NODE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <interface>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_INTERFACE_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_INTERFACE;
|
||||
else {
|
||||
log_error("Unexpected token in <interface>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_METHOD_NAME;
|
||||
else {
|
||||
log_error("Unexpected <method> attribute %s", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "arg"))
|
||||
state = STATE_METHOD_ARG;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <method> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "method")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <method> (1).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD;
|
||||
else {
|
||||
log_error("Unexpected token in <method> (2).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_METHOD_ARG_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_METHOD_ARG_TYPE;
|
||||
else if (streq_ptr(name, "direction"))
|
||||
state = STATE_METHOD_ARG_DIRECTION;
|
||||
else {
|
||||
log_error("Unexpected method <arg> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected method <arg> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "arg")))
|
||||
|
||||
state = STATE_METHOD;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in method <arg>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_METHOD_ARG_DIRECTION:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_METHOD_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in method <arg>. (4)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_SIGNAL_NAME;
|
||||
else {
|
||||
log_error("Unexpected <signal> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "arg"))
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <signal> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "signal")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <signal>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL;
|
||||
else {
|
||||
log_error("Unexpected token in <signal>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case STATE_SIGNAL_ARG:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_SIGNAL_ARG_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_SIGNAL_ARG_TYPE;
|
||||
else {
|
||||
log_error("Unexpected signal <arg> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected signal <arg> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "arg")))
|
||||
|
||||
state = STATE_SIGNAL;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in signal <arg> (1).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_ARG_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in signal <arg> (2).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_SIGNAL_ARG_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_SIGNAL_ARG;
|
||||
else {
|
||||
log_error("Unexpected token in signal <arg> (3).");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY:
|
||||
|
||||
if (t == XML_ATTRIBUTE_NAME) {
|
||||
if (streq_ptr(name, "name"))
|
||||
state = STATE_PROPERTY_NAME;
|
||||
else if (streq_ptr(name, "type"))
|
||||
state = STATE_PROPERTY_TYPE;
|
||||
else if (streq_ptr(name, "access"))
|
||||
state = STATE_PROPERTY_ACCESS;
|
||||
else {
|
||||
log_error("Unexpected <property> attribute %s.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (t == XML_TAG_OPEN) {
|
||||
|
||||
if (streq_ptr(name, "annotation")) {
|
||||
r = parse_xml_annotation(p, xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected <property> tag %s.", name);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
} else if (t == XML_TAG_CLOSE_EMPTY ||
|
||||
(t == XML_TAG_CLOSE && streq_ptr(name, "property")))
|
||||
|
||||
state = STATE_INTERFACE;
|
||||
|
||||
else if (t != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token in <property>. (1)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_NAME:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (2)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_TYPE:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (3)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STATE_PROPERTY_ACCESS:
|
||||
|
||||
if (t == XML_ATTRIBUTE_VALUE)
|
||||
state = STATE_PROPERTY;
|
||||
else {
|
||||
log_error("Unexpected token in <property>. (4)");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int find_nodes(sd_bus *bus, const char *service, const char *path, Set *paths) {
|
||||
const XMLIntrospectOps ops = {
|
||||
.on_path = on_path,
|
||||
};
|
||||
|
||||
_cleanup_bus_message_unref_ sd_bus_message *reply = NULL;
|
||||
_cleanup_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
const char *xml, *p;
|
||||
void *xml_state = NULL;
|
||||
_cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
const char *xml;
|
||||
int r;
|
||||
|
||||
r = sd_bus_call_method(bus, service, path, "org.freedesktop.DBus.Introspectable", "Introspect", &error, &reply, "");
|
||||
|
@ -861,37 +339,7 @@ static int find_nodes(sd_bus *bus, const char *service, const char *path, Set *p
|
|||
return bus_log_parse_error(r);
|
||||
|
||||
/* fputs(xml, stdout); */
|
||||
|
||||
p = xml;
|
||||
for (;;) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
|
||||
r = xml_tokenize(&p, &name, &xml_state, NULL);
|
||||
if (r < 0) {
|
||||
log_error("XML parse error");
|
||||
return r;
|
||||
}
|
||||
|
||||
if (r == XML_END)
|
||||
break;
|
||||
|
||||
if (r == XML_TAG_OPEN) {
|
||||
|
||||
if (streq(name, "node")) {
|
||||
r = parse_xml_node(path, paths, &p, &xml_state);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else {
|
||||
log_error("Unexpected tag '%s' in introspection data.", name);
|
||||
return -EBADMSG;
|
||||
}
|
||||
} else if (r != XML_TEXT || !in_charset(name, WHITESPACE)) {
|
||||
log_error("Unexpected token.");
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return parse_xml_introspect(path, xml, &ops, paths);
|
||||
}
|
||||
|
||||
static int tree_one(sd_bus *bus, const char *service, const char *prefix) {
|
||||
|
|
Loading…
Reference in a new issue