From: Daniel Golle Date: Mon, 7 Sep 2026 18:26:54 +0000 (+0100) Subject: fwupd: backport upstream support for MxL862xx switches X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=54aae64f112527444095fecd4f481b3856950eb0;p=openwrt-packages.git fwupd: backport upstream support for MxL862xx switches Backport two patches from upstream implementing devlink-based firmware update support for the MaxLinear MxL862xx DSA switch family, found eg. on the BananaPi R4 Pro (8E/4E). Users can update the switch firmware using `fwupdmgr update`. Signed-off-by: Daniel Golle --- diff --git a/utils/fwupd/Makefile b/utils/fwupd/Makefile index a88fa10a1..dfff2e607 100644 --- a/utils/fwupd/Makefile +++ b/utils/fwupd/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=fwupd PKG_VERSION:=2.1.7 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://github.com/fwupd/fwupd/releases/download/$(PKG_VERSION) diff --git a/utils/fwupd/patches/0001-devlink-Add-MDIO-bus-support.patch b/utils/fwupd/patches/0001-devlink-Add-MDIO-bus-support.patch new file mode 100644 index 000000000..f390abfe3 --- /dev/null +++ b/utils/fwupd/patches/0001-devlink-Add-MDIO-bus-support.patch @@ -0,0 +1,253 @@ +From 5298944fa6b14e9c6a36bc61af7e4a495033b7af Mon Sep 17 00:00:00 2001 +From: Daniel Golle +Date: Mon, 13 Jul 2026 22:55:10 +0100 +Subject: [PATCH 1/2] devlink: Add MDIO bus support + +The backend refused to create devices for anything but the "pci" and +"netdevsim" buses. Ethernet switches attached via MDIO (bus name +"mdio_bus"), such as the MaxLinear MxL862xx family, were rejected +before their versions were ever queried: + + failed to add devlink device mdio_bus/mdio-bus:10: unsupported bus + type: mdio_bus (only 'pci' and 'netdevsim' are supported) + +Create a parent device via the udev backend like it is already done +for PCI devices. MDIO devices expose no vendor or device IDs in sysfs, +so use the first "compatible" string of the associated OF node to +identify the device where available: the vendor prefix becomes a +devicetree vendor ID, e.g. DT:maxlinear, and the two halves of the +compatible are used as the VEN and DEV instance strings, so the usual +quirk instance ID, e.g. MDIO_BUS\VEN_maxlinear&DEV_mxl86252, and +component GUIDs, e.g. MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw, +are built just like for PCI devices. + +Unlike the versions reported by the kernel, the devicetree compatible +is constant and always available, even when the device cannot report +any version information, for example while it is stuck in a bootloader +rescue mode. Firmware targeting the compatible-based component GUID +can therefore still be matched and flashed to recover such a device. + +Signed-off-by: Daniel Golle +--- + plugins/devlink/README.md | 18 ++++++- + plugins/devlink/fu-devlink-backend.c | 81 +++++++++++++++++++++++++++- + plugins/devlink/fu-devlink-device.c | 22 ++++++++ + 3 files changed, 117 insertions(+), 4 deletions(-) + +--- a/plugins/devlink/README.md ++++ b/plugins/devlink/README.md +@@ -9,7 +9,8 @@ This is a generic plugin that can work w + + ## Supported Devices + +-The plugin supports any device that implements the devlink interface, regardless the bus it resides on. ++The plugin supports devices implementing the devlink interface on the PCI and ++MDIO buses, as well as emulated netdevsim devices for testing. + + ## Firmware Format + +@@ -25,9 +26,21 @@ This plugin supports the following proto + These devices use custom instance IDs consisting of the component name. + + * `PCI\VEN_15B3&DEV_1021&COMPONENT_fw` ++* `MDIO_BUS\COMPONENT_fw` ++* `MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw` + + Optionally, additional GUID might get generated as specified in the squirk file, see below. + ++For devices attached via MDIO with an associated devicetree node, the vendor ++prefix and device name from the first "compatible" string are used as the ++`VEN` and `DEV` instance ID components, resulting in a quirk-only ++`MDIO_BUS\VEN_maxlinear&DEV_mxl86252` instance ID on the devlink device and ++component GUIDs just like for PCI devices. Unlike the versions reported by ++the kernel, the devicetree compatible is available even when the device ++cannot report any version information, for example while it is stuck in a ++bootloader rescue mode, so firmware targeting the compatible-based GUID can ++still be matched and flashed to recover such a device. ++ + ### Device Identification + + Devices are identified using their in the format: +@@ -131,7 +144,8 @@ The plugin handles various error conditi + + ## Vendor ID Security + +-The vendor ID is set from the PCI vendor. ++The vendor ID is set from the PCI vendor, or for devices on the MDIO bus, from ++the devicetree vendor prefix, e.g. `DT:maxlinear`. + + ## External Interface Access + +--- a/plugins/devlink/fu-devlink-backend.c ++++ b/plugins/devlink/fu-devlink-backend.c +@@ -67,6 +67,71 @@ fu_devlink_backend_create_pci_parent(FuD + } + + static FuDevice * ++fu_devlink_backend_create_mdio_parent(FuDevlinkBackend *self, ++ const gchar *bus_name, ++ const gchar *dev_name, ++ GError **error) ++{ ++ FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); ++ FuBackend *udev_backend = NULL; ++ g_autofree gchar *mdio_sysfs_path = NULL; ++ g_autofree gchar *mdio_sysfs_real = NULL; ++ g_autoptr(FuDevice) mdio_device = NULL; ++ g_autoptr(GBytes) compatible_blob = NULL; ++ g_autoptr(GError) error_local = NULL; ++ ++ udev_backend = fu_context_get_backend_by_name(ctx, "udev", &error_local); ++ if (udev_backend == NULL) { ++ g_set_error(error, ++ FWUPD_ERROR, ++ FWUPD_ERROR_NOT_SUPPORTED, ++ "udev backend not available: %s", ++ error_local->message); ++ return NULL; ++ } ++ ++ mdio_sysfs_path = g_strdup_printf("/sys/bus/mdio_bus/devices/%s", dev_name); ++ mdio_sysfs_real = fu_path_make_absolute(mdio_sysfs_path, error); ++ if (mdio_sysfs_real == NULL) ++ return NULL; ++ ++ mdio_device = fu_backend_create_device(udev_backend, mdio_sysfs_real, &error_local); ++ if (mdio_device == NULL) { ++ g_set_error(error, ++ FWUPD_ERROR, ++ FWUPD_ERROR_NOT_FOUND, ++ "failed to create MDIO device for %s: %s", ++ mdio_sysfs_path, ++ error_local->message); ++ return NULL; ++ } ++ ++ if (!fu_device_probe(mdio_device, error)) { ++ g_prefix_error_literal(error, "failed to probe MDIO device: "); ++ return NULL; ++ } ++ ++ /* of_node/compatible is a NUL-separated string list, so read it as raw bytes */ ++ compatible_blob = fu_udev_device_read_sysfs_bytes(FU_UDEV_DEVICE(mdio_device), ++ "of_node/compatible", ++ -1, ++ FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, ++ NULL); ++ if (compatible_blob != NULL && g_bytes_get_size(compatible_blob) > 0) { ++ g_autofree gchar *compatible = g_strndup(g_bytes_get_data(compatible_blob, NULL), ++ g_bytes_get_size(compatible_blob)); ++ g_auto(GStrv) parts = g_strsplit(compatible, ",", 2); ++ if (g_strv_length(parts) == 2) { ++ fu_device_build_vendor_id(mdio_device, "DT", parts[0]); ++ fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]); ++ fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]); ++ } ++ } ++ ++ return g_steal_pointer(&mdio_device); ++} ++ ++static FuDevice * + fu_devlink_backend_create_netdevsim_parent(FuDevlinkBackend *self, + const gchar *bus_name, + const gchar *dev_name, +@@ -96,6 +161,7 @@ fu_devlink_backend_device_added(FuDevlin + { + FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); + FuDevice *old_devlink_device; ++ const gchar *instance_keys[] = {"VEN", "DEV", NULL}; + g_autoptr(FuDevice) devlink_device = NULL; + g_autoptr(FuDevice) parent_device = NULL; + +@@ -104,12 +170,17 @@ fu_devlink_backend_device_added(FuDevlin + g_return_val_if_fail(dev_name != NULL, NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + +- /* only support PCI and netdevsim buses */ ++ /* create a bus-specific parent device */ + if (g_strcmp0(bus_name, "pci") == 0) { + parent_device = + fu_devlink_backend_create_pci_parent(self, bus_name, dev_name, error); + if (parent_device == NULL) + return NULL; ++ } else if (g_strcmp0(bus_name, "mdio_bus") == 0) { ++ parent_device = ++ fu_devlink_backend_create_mdio_parent(self, bus_name, dev_name, error); ++ if (parent_device == NULL) ++ return NULL; + } else if (g_strcmp0(bus_name, "netdevsim") == 0) { + parent_device = + fu_devlink_backend_create_netdevsim_parent(self, bus_name, dev_name, error); +@@ -119,7 +190,8 @@ fu_devlink_backend_device_added(FuDevlin + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, +- "unsupported bus type: %s (only 'pci' and 'netdevsim' are supported)", ++ "unsupported bus type: %s (only 'pci', 'mdio_bus' and 'netdevsim' are " ++ "supported)", + bus_name); + return NULL; + } +@@ -157,6 +229,11 @@ fu_devlink_backend_device_added(FuDevlin + FU_DEVICE_INCORPORATE_FLAG_VENDOR | + FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | + FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID); ++ for (guint i = 0; instance_keys[i] != NULL; i++) { ++ const gchar *value = fu_device_get_instance_str(parent_device, instance_keys[i]); ++ if (value != NULL) ++ fu_device_add_instance_str(devlink_device, instance_keys[i], value); ++ } + + /* only add the devlink device to the backend - parent is managed by its own backend */ + fu_backend_device_added(FU_BACKEND(self), devlink_device); +--- a/plugins/devlink/fu-devlink-device.c ++++ b/plugins/devlink/fu-devlink-device.c +@@ -699,6 +699,9 @@ fu_devlink_device_setup(FuDevice *device + if (fu_device_get_vid(device) != 0 && fu_device_get_pid(device) != 0) { + fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(device)); + fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(device)); ++ } ++ if (fu_device_get_instance_str(device, "VEN") != NULL && ++ fu_device_get_instance_str(device, "DEV") != NULL) { + if (!fu_device_build_instance_id_full(device, + FU_DEVICE_INSTANCE_FLAG_QUIRKS, + error, +@@ -864,6 +867,17 @@ fu_devlink_device_add_json(FuDevice *dev + fwupd_json_object_add_string(json_obj, "BusName", self->bus_name); + if (self->dev_name != NULL) + fwupd_json_object_add_string(json_obj, "DevName", self->dev_name); ++ ++ /* instance strings inherited from the parent device, which does not exist under ++ * emulation */ ++ if (fu_device_get_instance_str(device, "VEN") != NULL) ++ fwupd_json_object_add_string(json_obj, ++ "Ven", ++ fu_device_get_instance_str(device, "VEN")); ++ if (fu_device_get_instance_str(device, "DEV") != NULL) ++ fwupd_json_object_add_string(json_obj, ++ "Dev", ++ fu_device_get_instance_str(device, "DEV")); + } + + static gboolean +@@ -872,11 +886,19 @@ fu_devlink_device_from_json(FuDevice *de + FuDevlinkDevice *self = FU_DEVLINK_DEVICE(device); + const gchar *bus_name; + const gchar *dev_name; ++ const gchar *ven; ++ const gchar *dev; + g_autofree gchar *device_id = NULL; + + /* devlink-specific properties */ + bus_name = fwupd_json_object_get_string(json_obj, "BusName", NULL); + dev_name = fwupd_json_object_get_string(json_obj, "DevName", NULL); ++ ven = fwupd_json_object_get_string(json_obj, "Ven", NULL); ++ if (ven != NULL) ++ fu_device_add_instance_str(device, "VEN", ven); ++ dev = fwupd_json_object_get_string(json_obj, "Dev", NULL); ++ if (dev != NULL) ++ fu_device_add_instance_str(device, "DEV", dev); + + if (bus_name == NULL || dev_name == NULL) { + g_set_error_literal(error, diff --git a/utils/fwupd/patches/0002-devlink-Add-quirks-for-MaxLinear-MxL862xx-Ethernet-s.patch b/utils/fwupd/patches/0002-devlink-Add-quirks-for-MaxLinear-MxL862xx-Ethernet-s.patch new file mode 100644 index 000000000..0a801f783 --- /dev/null +++ b/utils/fwupd/patches/0002-devlink-Add-quirks-for-MaxLinear-MxL862xx-Ethernet-s.patch @@ -0,0 +1,228 @@ +From e50c9e5ab39d31242e664efbbf441fd46d15a0cd Mon Sep 17 00:00:00 2001 +From: Daniel Golle +Date: Mon, 13 Jul 2026 22:57:10 +0100 +Subject: [PATCH 2/2] devlink: Add quirks for MaxLinear MxL862xx Ethernet + switches + +The mxl862xx DSA driver implements devlink info_get and flash_update +for these switches attached via MDIO. The chip ID registers are only +readable with a running firmware, so a switch stuck in MCUboot rescue +mode reports no fixed versions at all and cannot be identified by its +numeric "asic.id". Match the device and its firmware component on the +devicetree compatible instead: it is available in both modes, so +firmware targeting the compatible-based component GUID can also be +flashed to recover a switch from rescue mode. + +The vendor ID derived from the devicetree vendor prefix by the MDIO +parent device only reaches the devlink device itself, so it is +repeated here for the component. + +The firmware image contains both application slots and is flashed as a +whole, so component-specific updates are rejected by the driver and +omit-component-name is required. All ports go down for the duration of +the flash process and the new image boots immediately afterwards, +hence neither usable-during-update nor dual-image apply. + +Also, add emulations recorded on a Bananapi BPI-R4 Pro 8X with a MaxLinear +MxL86252 Ethernet switch attached via MDIO. + +Signed-off-by: Daniel Golle +--- + plugins/devlink/README.md | 2 +- + plugins/devlink/devlink.quirk | 18 +++++++++ + plugins/devlink/fu-devlink-backend.c | 41 +++++++++++++++------ + plugins/devlink/fu-devlink-device.c | 21 +++++++---- + plugins/devlink/meson.build | 5 ++- + plugins/devlink/tests/devlink-mxl86252.json | 20 ++++++++++ + 6 files changed, 86 insertions(+), 21 deletions(-) + create mode 100644 plugins/devlink/tests/devlink-mxl86252.json + +--- a/plugins/devlink/README.md ++++ b/plugins/devlink/README.md +@@ -29,7 +29,7 @@ These devices use custom instance IDs co + * `MDIO_BUS\COMPONENT_fw` + * `MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw` + +-Optionally, additional GUID might get generated as specified in the squirk file, see below. ++Optionally, additional GUIDs might be generated as specified in the quirk file, see below. + + For devices attached via MDIO with an associated devicetree node, the vendor + prefix and device name from the first "compatible" string are used as the +--- a/plugins/devlink/devlink.quirk ++++ b/plugins/devlink/devlink.quirk +@@ -27,3 +27,21 @@ DevlinkFixedVersions = fw.psid + # NVIDIA ConnectX-8 + [PCI\VEN_15B3&DEV_1023] + DevlinkFixedVersions = fw.psid ++ ++# MaxLinear MxL86252 Ethernet switch ++[MDIO_BUS\VEN_maxlinear&DEV_mxl86252] ++Name = MxL86252 Ethernet Switch ++Vendor = MaxLinear ++[MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw] ++Vendor = MaxLinear ++VendorId = DT:maxlinear ++Flags = omit-component-name,~usable-during-update,~dual-image ++ ++# MaxLinear MxL86282 Ethernet switch ++[MDIO_BUS\VEN_maxlinear&DEV_mxl86282] ++Name = MxL86282 Ethernet Switch ++Vendor = MaxLinear ++[MDIO_BUS\VEN_maxlinear&DEV_mxl86282&COMPONENT_fw] ++Vendor = MaxLinear ++VendorId = DT:maxlinear ++Flags = omit-component-name,~usable-during-update,~dual-image +--- a/plugins/devlink/fu-devlink-backend.c ++++ b/plugins/devlink/fu-devlink-backend.c +@@ -40,7 +40,16 @@ fu_devlink_backend_create_pci_parent(FuD + } + + /* construct PCI sysfs path from bus_name (e.g., "pci/0000:01:00.0") */ +- pci_sysfs_path = g_strdup_printf("/sys/bus/pci/devices/%s", dev_name); ++ pci_sysfs_path = fu_context_build_filename(ctx, ++ error, ++ FU_PATH_KIND_SYSFSDIR, ++ "bus", ++ "pci", ++ "devices", ++ dev_name, ++ NULL); ++ if (pci_sysfs_path == NULL) ++ return NULL; + pci_sysfs_real = fu_path_make_absolute(pci_sysfs_path, error); + if (pci_sysfs_real == NULL) + return NULL; +@@ -90,7 +99,16 @@ fu_devlink_backend_create_mdio_parent(Fu + return NULL; + } + +- mdio_sysfs_path = g_strdup_printf("/sys/bus/mdio_bus/devices/%s", dev_name); ++ mdio_sysfs_path = fu_context_build_filename(ctx, ++ error, ++ FU_PATH_KIND_SYSFSDIR, ++ "bus", ++ "mdio_bus", ++ "devices", ++ dev_name, ++ NULL); ++ if (mdio_sysfs_path == NULL) ++ return NULL; + mdio_sysfs_real = fu_path_make_absolute(mdio_sysfs_path, error); + if (mdio_sysfs_real == NULL) + return NULL; +@@ -118,13 +136,14 @@ fu_devlink_backend_create_mdio_parent(Fu + FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, + NULL); + if (compatible_blob != NULL && g_bytes_get_size(compatible_blob) > 0) { +- g_autofree gchar *compatible = g_strndup(g_bytes_get_data(compatible_blob, NULL), +- g_bytes_get_size(compatible_blob)); +- g_auto(GStrv) parts = g_strsplit(compatible, ",", 2); +- if (g_strv_length(parts) == 2) { +- fu_device_build_vendor_id(mdio_device, "DT", parts[0]); +- fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]); +- fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]); ++ g_autofree gchar *compatible = fu_strsafe_bytes(compatible_blob, G_MAXSIZE); ++ if (compatible != NULL) { ++ g_auto(GStrv) parts = g_strsplit(compatible, ",", 2); ++ if (g_strv_length(parts) == 2) { ++ fu_device_build_vendor_id(mdio_device, "DT", parts[0]); ++ fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]); ++ fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]); ++ } + } + } + +@@ -161,7 +180,7 @@ fu_devlink_backend_device_added(FuDevlin + { + FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); + FuDevice *old_devlink_device; +- const gchar *instance_keys[] = {"VEN", "DEV", NULL}; ++ const gchar *instance_keys[] = {"VEN", "DEV"}; + g_autoptr(FuDevice) devlink_device = NULL; + g_autoptr(FuDevice) parent_device = NULL; + +@@ -229,7 +248,7 @@ fu_devlink_backend_device_added(FuDevlin + FU_DEVICE_INCORPORATE_FLAG_VENDOR | + FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | + FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID); +- for (guint i = 0; instance_keys[i] != NULL; i++) { ++ for (guint i = 0; i < G_N_ELEMENTS(instance_keys); i++) { + const gchar *value = fu_device_get_instance_str(parent_device, instance_keys[i]); + if (value != NULL) + fu_device_add_instance_str(devlink_device, instance_keys[i], value); +--- a/plugins/devlink/fu-devlink-device.c ++++ b/plugins/devlink/fu-devlink-device.c +@@ -868,16 +868,17 @@ fu_devlink_device_add_json(FuDevice *dev + if (self->dev_name != NULL) + fwupd_json_object_add_string(json_obj, "DevName", self->dev_name); + +- /* instance strings inherited from the parent device, which does not exist under +- * emulation */ +- if (fu_device_get_instance_str(device, "VEN") != NULL) ++ /* instance strings inherited from the parent, which does not exist under emulation */ ++ if (fu_device_get_instance_str(device, "VEN") != NULL) { + fwupd_json_object_add_string(json_obj, +- "Ven", ++ "Vid", + fu_device_get_instance_str(device, "VEN")); +- if (fu_device_get_instance_str(device, "DEV") != NULL) ++ } ++ if (fu_device_get_instance_str(device, "DEV") != NULL) { + fwupd_json_object_add_string(json_obj, +- "Dev", ++ "Pid", + fu_device_get_instance_str(device, "DEV")); ++ } + } + + static gboolean +@@ -893,10 +894,14 @@ fu_devlink_device_from_json(FuDevice *de + /* devlink-specific properties */ + bus_name = fwupd_json_object_get_string(json_obj, "BusName", NULL); + dev_name = fwupd_json_object_get_string(json_obj, "DevName", NULL); +- ven = fwupd_json_object_get_string(json_obj, "Ven", NULL); ++ ven = fwupd_json_object_get_string(json_obj, "Vid", NULL); ++ if (ven == NULL) ++ ven = fwupd_json_object_get_string(json_obj, "Ven", NULL); + if (ven != NULL) + fu_device_add_instance_str(device, "VEN", ven); +- dev = fwupd_json_object_get_string(json_obj, "Dev", NULL); ++ dev = fwupd_json_object_get_string(json_obj, "Pid", NULL); ++ if (dev == NULL) ++ dev = fwupd_json_object_get_string(json_obj, "Dev", NULL); + if (dev != NULL) + fu_device_add_instance_str(device, "DEV", dev); + +--- a/plugins/devlink/meson.build ++++ b/plugins/devlink/meson.build +@@ -29,6 +29,7 @@ plugin_builtin_devlink = static_library( + plugin_builtins += plugin_builtin_devlink + + device_tests += files( ++ 'tests/devlink-mxl86252.json', + 'tests/devlink-netdevsim.json', + ) + +--- /dev/null ++++ b/plugins/devlink/tests/devlink-mxl86252.json +@@ -0,0 +1,20 @@ ++{ ++ "name": "MaxLinear MxL86252 Ethernet Switch Update", ++ "interactive": false, ++ "steps": [ ++ { ++ "url": "972ec047263bc0e3e06a9fa929c65923f1cae6ead59e0904d4fe7440c5e6847a-mxl862xxc-1085-signed-upgrade-dsa.cab", ++ "emulation-url": "c2973f267c338b9904238c9964f612260f789248d711247bda6485bf4750d2b4-mxl86252-emulation.zip", ++ "components": [ ++ { ++ "name": "fw", ++ "protocol": "org.kernel.devlink", ++ "version": "1.0.85", ++ "guids": [ ++ "d2a6f602-d988-5425-b686-5c1081576cce" ++ ] ++ } ++ ] ++ } ++ ] ++}