From f842c8c8d0d809b298668c9f547e6df837532fce Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Wed, 12 Jan 2022 02:57:58 +0100 Subject: libosmo-gtlv: add auto dec/enc to/from structs Add osmo_gtlv_coding: describe the value part of a TLV (decode and encode), describe a struct with its members, and get/put readily decoded structs from/to a raw PDU, directly. With osmo_gtlv_coding defined for a protocol's tags, we only deal with encoded PDUs or fully decoded C structs, no TLV related re-implementations clutter up the message handling code. A usage example is given in gtlv_dec_enc_test. The first real use will be the PFCP protocol in osmo-upf.git. With osmo_gtlv_coding, there still is a lot of monkey work involved in describing the decoded structs. A subsequent patch adds a generator for osmo_gtlv_coding and message structs from tag value lists. Related: SYS#5599 Change-Id: I65de793105882a452124ee58adb0e58469e6e796 --- src/libosmo-gtlv/Makefile.am | 1 + src/libosmo-gtlv/gtlv_dec_enc.c | 517 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 src/libosmo-gtlv/gtlv_dec_enc.c (limited to 'src') diff --git a/src/libosmo-gtlv/Makefile.am b/src/libosmo-gtlv/Makefile.am index 7ebfb60..bcaff33 100644 --- a/src/libosmo-gtlv/Makefile.am +++ b/src/libosmo-gtlv/Makefile.am @@ -22,4 +22,5 @@ noinst_LIBRARIES = \ libosmo_gtlv_a_SOURCES = \ gtlv.c \ + gtlv_dec_enc.c \ $(NULL) diff --git a/src/libosmo-gtlv/gtlv_dec_enc.c b/src/libosmo-gtlv/gtlv_dec_enc.c new file mode 100644 index 0000000..28ee493 --- /dev/null +++ b/src/libosmo-gtlv/gtlv_dec_enc.c @@ -0,0 +1,517 @@ +/* + * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved. + * + * Author: Neels Janosch Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +#include + +#include + +/* Reverse offsetof(): return the address of the struct member for a given osmo_gtlv_msg and member ofs_foo value. */ +#define MEMB(M, MEMB_OFS) ((void *)((char *)(M) + (MEMB_OFS))) + +#define RETURN_ERROR(RC, IEI, FMT, ARGS...) \ + do {\ + if (err_cb) \ + err_cb(err_cb_data, (void *)decoded_struct, __FILE__, __LINE__, \ + "IE '%s' (0x%x): " FMT " (%d: %s)\n", \ + get_value_string(iei_strs, IEI), IEI, ##ARGS, RC, strerror((RC) > 0 ? (RC) : -(RC))); \ + return RC; \ + } while (0) + + +/*! Decode a TLV structure from raw data to a decoded struct, for unordered TLV IEs. + * How to decode IE values and where to place them in the decoded struct, is defined by ie_coding, an array terminated + * by a '{}' entry. + * The IEs may appear in any ordering in the TLV data. + * For unordered decoding, only IEs with has_presence_flag == true or has_count == true may repeat. Other IE definitions + * cause the last read TLV to overwrite all previous decodings, all into the first occurrence in ie_coding. + * \param[out] decoded_struct Pointer to the struct to write parsed IE data to. + * \param[in] obj_ofs Pass as zero. Used for nested IEs: offset added to decoded_struct to get to a sub-struct. + * \param[in] gtlv TLV data to parse, as given in gtlv->msg.*. Must be ready for osmo_gtlv_load_start(). + * \param[in] ie_coding A list of permitted/expected IEI tags and instructions for decoding. + * \param[in] err_cb Function to call to report an error message, or NULL. + * \param[in] err_cb_data Caller supplied context to pass to the err_cb as 'data' argument. + * \param[in] iei_strs value_string array to give IEI names in error messages passed to err_cb(), or NULL. + * \return 0 on success, negative on error. + */ +static int osmo_gtlvs_decode_unordered(void *decoded_struct, unsigned int obj_ofs, struct osmo_gtlv_load *gtlv, + const struct osmo_gtlv_coding *ie_coding, + osmo_gtlv_err_cb err_cb, void *err_cb_data, const struct value_string *iei_strs) +{ + void *obj = MEMB(decoded_struct, obj_ofs); + const struct osmo_gtlv_coding *iec; + unsigned int *multi_count_p; + + /* To check for presence of mandatory IEs, need to keep a flag stack of seen ie_coding entries. This array has + * to have at least the nr of entries that the ie_coding array has. Let's allow up to this many ie_coding + * entries to avoid dynamic allocation. Seems like enough. */ + bool seen_ie_coding_entries[4096] = {}; + bool *seen_p; +#define CHECK_SEEN(IEC) do { \ + unsigned int ie_coding_idx = (IEC) - ie_coding; \ + if (ie_coding_idx >= ARRAY_SIZE(seen_ie_coding_entries)) \ + RETURN_ERROR(-ENOTSUP, gtlv->tag, \ + "Too many IE definitions for decoding an unordered TLV structure"); \ + seen_p = &seen_ie_coding_entries[ie_coding_idx]; \ + } while (0) + + + osmo_gtlv_load_start(gtlv); + + /* IEs are allowed to come in any order. So traverse the TLV structure once, and find an IE parser for each (if + * any). */ + for (;;) { + int rc; + bool *presence_flag_p; + unsigned int memb_next_array_idx; + unsigned int memb_ofs; + unsigned int ie_max_allowed_count; + + rc = osmo_gtlv_load_next(gtlv); + if (rc) + RETURN_ERROR(rc, gtlv->tag, "Decoding IEs failed on or after this tag"); + if (!gtlv->val) { + /* End of the TLV structure */ + break; + } + + /* ie_max_allowed_count counts how often the same IEI may appear in a message until all struct members + * that can store them are filled up. */ + ie_max_allowed_count = 0; + + do { + /* Find the IE coding for this tag */ + for (iec = ie_coding; !osmo_gtlv_coding_end(iec) && iec->tag != gtlv->tag; iec++); + /* No such IE coding found. */ + if (osmo_gtlv_coding_end(iec)) + break; + + /* Keep track how often this tag can occur */ + ie_max_allowed_count += iec->has_count ? iec->count_max : 1; + + /* Was this iec instance already decoded? Then skip to the next one, if any. */ + presence_flag_p = iec->has_presence_flag ? MEMB(obj, iec->presence_flag_ofs) : NULL; + multi_count_p = iec->has_count ? MEMB(obj, iec->count_ofs) : NULL; + if ((presence_flag_p && *presence_flag_p) + || (multi_count_p && *multi_count_p >= iec->count_max)) + continue; + /* For IEs with a presence flag or a multi count, the decoded struct provides the information + * whether the IE has already been decoded. Do the same for mandatory IEs, using local state in + * seen_ie_coding_entries[]. */ + CHECK_SEEN(iec); + if (*seen_p) + continue; + } while (0); + if (osmo_gtlv_coding_end(iec)) { + if (ie_max_allowed_count) { + /* There have been IE definitions for this IEI, but all slots to decode it are already + * filled. */ + RETURN_ERROR(-ENOTSUP, gtlv->tag, "Only %u instances of this IE are supported per message", + ie_max_allowed_count); + } + /* No such IE defined in ie_coding, just skip the TLV. */ + continue; + } + + /* If this is a repeated IE, decode into the correct array index memb[idx], + * next idx == (*multi_count_p). We've already guaranteed above that *multi_count_p < count_max. */ + memb_next_array_idx = multi_count_p ? *multi_count_p : 0; + memb_ofs = iec->memb_ofs + memb_next_array_idx * iec->memb_array_pitch; + + /* Decode IE value part */ + if (iec->nested_ies) { + /* A nested IE: the value part of this TLV is in turn a TLV structure. Decode the inner + * IEs. */ + struct osmo_gtlv_load inner_tlv = { + .cfg = iec->nested_ies_cfg ? : gtlv->cfg, + .src = { + .data = gtlv->val, + .len = gtlv->len, + } + }; + bool ordered; + switch (iec->nested_ies_ordered) { + case OSMO_GTLV_NESTED_IES_ORDERED: + ordered = true; + break; + case OSMO_GTLV_NESTED_IES_ORDERING_SAME: + case OSMO_GTLV_NESTED_IES_UNORDERED: + ordered = false; + break; + default: + OSMO_ASSERT(0); + } + rc = osmo_gtlvs_decode(decoded_struct, obj_ofs + memb_ofs, &inner_tlv, ordered, iec->nested_ies, + err_cb, err_cb_data, iei_strs); + if (rc) + RETURN_ERROR(rc, gtlv->tag, "Error while decoding TLV structure nested inside this IE"); + } else { + /* Normal IE, decode the specific IE data. */ + if (!iec->dec_func) + RETURN_ERROR(-EIO, gtlv->tag, "IE definition lacks a dec_func()"); + rc = iec->dec_func(decoded_struct, MEMB(obj, memb_ofs), gtlv); + if (rc) + RETURN_ERROR(rc, gtlv->tag, "Error while decoding this IE"); + } + + if (multi_count_p) { + /* A repeated IE, record that we've added one entry. This increments the foo_count value in the + * decoded osmo_gtlv_msg.ies.*. + * For example, multi_count_p points at osmo_gtlv_msg_session_est_req.create_pdr_count, + * and memb_ofs points at osmo_gtlv_msg_session_est_req.create_pdr. */ + (*multi_count_p)++; + } + if (presence_flag_p) { + *presence_flag_p = true; + } + CHECK_SEEN(iec); + *seen_p = true; + } + + /* Check presence of mandatory IEs */ + for (iec = ie_coding; !osmo_gtlv_coding_end(iec); iec++) { + if (iec->has_presence_flag) + continue; + multi_count_p = iec->has_count ? MEMB(obj, iec->count_ofs) : NULL; + if (multi_count_p) { + if (*multi_count_p < iec->count_mandatory) + RETURN_ERROR(-EINVAL, iec->tag, "Error while decoding: %u instances of this IE are mandatory, got %u", + iec->count_mandatory, *multi_count_p); + continue; + } + /* Neither an optional nor a multi member, hence it must be mandatory. */ + CHECK_SEEN(iec); + if (!*seen_p) + RETURN_ERROR(-EINVAL, iec->tag, "Missing mandatory IE"); + } + return 0; +} + +/*! Decode a TLV structure from raw data to a decoded struct, for ordered TLV IEs. + * How to decode IE values and where to place them in the decoded struct, is defined by ie_coding, an array terminated + * by a '{}' entry. + * The IEs in the TLV structure must appear in the same order as they are defined in ie_coding. + * cause the last read TLV to overwrite all previous decodings, all into the first occurrence in ie_coding. + * \param[out] decoded_struct Pointer to the struct to write parsed IE data to. + * \param[in] obj_ofs Pass as zero. Used for nested IEs: offset added to decoded_struct to get to a sub-struct. + * \param[in] gtlv TLV data to parse, as given in gtlv->msg.*. Must be ready for osmo_gtlv_load_start(). + * \param[in] ie_coding A list of permitted/expected IEI tags and instructions for decoding. + * \param[in] err_cb Function to call to report an error message, or NULL. + * \param[in] err_cb_data Caller supplied context to pass to the err_cb as 'data' argument. + * \param[in] iei_strs value_string array to give IEI names in error messages passed to err_cb(), or NULL. + * \return 0 on success, negative on error. + */ +static int osmo_gtlvs_decode_ordered(void *decoded_struct, unsigned int obj_ofs, struct osmo_gtlv_load *gtlv, + const struct osmo_gtlv_coding *ie_coding, + osmo_gtlv_err_cb err_cb, void *err_cb_data, const struct value_string *iei_strs) +{ + void *obj = MEMB(decoded_struct, obj_ofs); + + osmo_gtlv_load_start(gtlv); + + for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { + int rc; + bool *presence_flag = ie_coding->has_presence_flag ? MEMB(obj, ie_coding->presence_flag_ofs) : NULL; + unsigned int *multi_count = ie_coding->has_count ? MEMB(obj, ie_coding->count_ofs) : NULL; + + rc = osmo_gtlv_load_next_by_tag(gtlv, ie_coding->tag); + switch (rc) { + case 0: + break; + case -ENOENT: + if (!presence_flag && (!multi_count || *multi_count < ie_coding->count_mandatory)) + RETURN_ERROR(rc, ie_coding->tag, "Missing mandatory IE"); + if (presence_flag) + *presence_flag = false; + continue; + default: + RETURN_ERROR(rc, ie_coding->tag, "Error in TLV structure"); + } + + for (;;) { + /* If this is a repeated IE, decode into the correct array index memb[idx], + * next idx == (*multi_count) */ + unsigned int memb_next_array_idx = multi_count ? *multi_count : 0; + unsigned int memb_ofs = ie_coding->memb_ofs + memb_next_array_idx * ie_coding->memb_array_pitch; + + if (multi_count && memb_next_array_idx >= ie_coding->count_max) + RETURN_ERROR(-ENOTSUP, ie_coding->tag, "Only %u instances of this IE are supported per message", + ie_coding->count_max); + + /* Decode IE value part */ + if (ie_coding->nested_ies) { + /* A nested IE: the value part of this TLV is in turn a TLV structure. Decode the inner + * IEs. */ + struct osmo_gtlv_load inner_tlv = { + .cfg = ie_coding->nested_ies_cfg ? : gtlv->cfg, + .src = { + .data = gtlv->val, + .len = gtlv->len, + } + }; + bool ordered; + switch (ie_coding->nested_ies_ordered) { + case OSMO_GTLV_NESTED_IES_ORDERING_SAME: + case OSMO_GTLV_NESTED_IES_ORDERED: + ordered = true; + break; + case OSMO_GTLV_NESTED_IES_UNORDERED: + ordered = false; + break; + default: + OSMO_ASSERT(0); + } + rc = osmo_gtlvs_decode(decoded_struct, obj_ofs + memb_ofs, &inner_tlv, ordered, + ie_coding->nested_ies, err_cb, err_cb_data, iei_strs); + if (rc) + RETURN_ERROR(rc, ie_coding->tag, + "Error while decoding TLV structure nested inside this IE"); + } else { + /* Normal IE, decode the specific IE data. */ + if (!ie_coding->dec_func) + RETURN_ERROR(-EIO, ie_coding->tag, "IE definition lacks a dec_func()"); + rc = ie_coding->dec_func(decoded_struct, MEMB(obj, memb_ofs), gtlv); + if (rc) + RETURN_ERROR(rc, ie_coding->tag, "Error while decoding this IE"); + } + + if (presence_flag) + *presence_flag = true; + + if (!multi_count) { + /* Not a repeated IE. */ + break; + } + + /* A repeated IE, record that we've added one entry. This increments the foo_count value in the + * decoded osmo_pfcp_msg.ies.*. + * For example, multi_count points at osmo_pfcp_msg_session_est_req.create_pdr_count, + * and memb_ofs points at osmo_pfcp_msg_session_est_req.create_pdr. */ + (*multi_count)++; + + /* Does another one of these IEs follow? */ + if (osmo_gtlv_load_peek_tag(gtlv) != gtlv->tag) { + /* Next tag is a different IE, end the repetition. */ + break; + } + + /* continue, parsing the next repetition of this tag. */ + rc = osmo_gtlv_load_next(gtlv); + if (rc) + return rc; + } + /* continue parsing the next tag. */ + } + return 0; +} + +/*! Decode an entire TLV message from raw data to decoded struct. + * How to decode IE values and where to put them in the decoded struct is defined by ie_coding, an array terminated by + * a '{}' entry. + * \param[out] decoded_struct Pointer to the struct to write parsed IE data to. + * \param[in] obj_ofs Pass as zero. Used for nested IEs: offset added to decoded_struct to get to a sub-struct. + * \param[in] gtlv TLV data to parse, as given in gtlv->msg.*. Must be ready for osmo_gtlv_load_start(). + * \param[in] ie_coding A list of permitted/expected IEI tags and instructions for decoding. + * \param[in] err_cb Function to call to report an error message, or NULL. + * \param[in] err_cb_data Caller supplied context to pass to the err_cb as 'data' argument. + * \param[in] iei_strs value_string array to give IEI names in error messages passed to err_cb(), or NULL. + * \return 0 on success, negative on error. + */ +int osmo_gtlvs_decode(void *decoded_struct, unsigned int obj_ofs, struct osmo_gtlv_load *gtlv, bool tlv_ordered, + const struct osmo_gtlv_coding *ie_coding, + osmo_gtlv_err_cb err_cb, void *err_cb_data, const struct value_string *iei_strs) +{ + if (!ie_coding) + return -ENOTSUP; + if (tlv_ordered) + return osmo_gtlvs_decode_ordered(decoded_struct, obj_ofs, gtlv, ie_coding, err_cb, err_cb_data, iei_strs); + else + return osmo_gtlvs_decode_unordered(decoded_struct, obj_ofs, gtlv, ie_coding, err_cb, err_cb_data, + iei_strs); +} + +/*! Encode a TLV structure from decoded struct to raw data. + * How to encode IE values and where to read them in the decoded struct is defined by ie_coding, an array terminated by + * a '{}' entry. + * The IEs will be encoded in the order they appear in ie_coding. + * \param[out] gtlv Write data using this TLV definition to gtlv->dst. + * \param[in] decoded_struct C struct data to encode. + * \param[in] obj_ofs Nesting offset, pass as 0. + * \param[in] ie_coding A {} terminated list of IEI tags to encode (if present) and instructions for encoding. + * \param[in] err_cb Function to call to report an error message, or NULL. + * \param[in] err_cb_data Caller supplied context to pass to the err_cb as 'data' argument. + * \param[in] iei_strs value_string array to give IEI names in error messages passed to err_cb(), or NULL. + * \return 0 on success, negative on error. + */ +int osmo_gtlvs_encode(struct osmo_gtlv_put *gtlv, const void *decoded_struct, unsigned int obj_ofs, + const struct osmo_gtlv_coding *ie_coding, + osmo_gtlv_err_cb err_cb, void *err_cb_data, const struct value_string *iei_strs) +{ + void *obj = MEMB(decoded_struct, obj_ofs); + + if (!ie_coding) + return -ENOTSUP; + + for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { + int rc; + bool *presence_flag_p = ie_coding->has_presence_flag ? MEMB(obj, ie_coding->presence_flag_ofs) : NULL; + unsigned int *multi_count_p = ie_coding->has_count ? MEMB(obj, ie_coding->count_ofs) : NULL; + unsigned int n; + unsigned int i; + + if (presence_flag_p && !*presence_flag_p) + continue; + + if (multi_count_p) { + n = *multi_count_p; + if (!ie_coding->memb_array_pitch) + RETURN_ERROR(-EFAULT, ie_coding->tag, + "Error in protocol definition: The ie_coding lacks a memb_array_pitch" + " value, cannot be used as multi-IE\n"); + } else { + n = 1; + } + + for (i = 0; i < n; i++) { + unsigned int memb_ofs; + + osmo_gtlv_put_tl(gtlv, ie_coding->tag, 0); + + /* If this is a repeated IE, encode from the correct array index */ + if (multi_count_p && i >= ie_coding->count_max) + RETURN_ERROR(-ENOTSUP, ie_coding->tag, + "Only %u instances of this IE are supported per message", ie_coding->count_max); + memb_ofs = ie_coding->memb_ofs + i * ie_coding->memb_array_pitch; + + if (ie_coding->nested_ies) { + struct osmo_gtlv_put nested_tlv = { + .cfg = ie_coding->nested_ies_cfg ? : gtlv->cfg, + .dst = gtlv->dst, + }; + rc = osmo_gtlvs_encode(&nested_tlv, decoded_struct, obj_ofs + memb_ofs, + ie_coding->nested_ies, err_cb, err_cb_data, iei_strs); + if (rc) + RETURN_ERROR(rc, ie_coding->tag, + "Error while encoding TLV structure nested inside this IE"); + } else { + rc = ie_coding->enc_func(gtlv, decoded_struct, MEMB(obj, memb_ofs)); + if (rc) + RETURN_ERROR(rc, ie_coding->tag, "Error while encoding this IE"); + } + + osmo_gtlv_put_update_tl(gtlv); + } + } + return 0; +} + +/*! Compose a human readable string describing a decoded struct. + * How to encode IE values and where to read them in the decoded struct is defined by ie_coding, an array terminated by + * a '{}' entry. + * The IEs will be encoded in the order they appear in ie_coding. + * \param[out] buf Return the string in this buffer. + * \param[in] buflen Size of buf. + * \param[in] decoded_struct C struct data to encode. + * \param[in] obj_ofs Nesting offset, pass as 0. + * \param[in] ie_coding A {} terminated list of IEI tags to encode (if present) and instructions for encoding. + * \param[in] iei_strs value_string array to give IEI names in tag headers, or NULL. + * \return number of characters that would be written if the buffer is large enough, like snprintf(). + */ +int osmo_gtlvs_encode_to_str_buf(char *buf, size_t buflen, const void *decoded_struct, unsigned int obj_ofs, + const struct osmo_gtlv_coding *ie_coding, const struct value_string *iei_strs) +{ + struct osmo_strbuf sb = { .buf = buf, .len = buflen }; + + void *obj = MEMB(decoded_struct, obj_ofs); + + if (!ie_coding) + return -ENOTSUP; + + for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { + bool *presence_flag_p = ie_coding->has_presence_flag ? MEMB(obj, ie_coding->presence_flag_ofs) : NULL; + unsigned int *multi_count_p = ie_coding->has_count ? MEMB(obj, ie_coding->count_ofs) : NULL; + unsigned int n; + unsigned int i; + + if (presence_flag_p && !*presence_flag_p) + continue; + + if (multi_count_p) { + n = *multi_count_p; + } else { + n = 1; + } + + if (!n) + continue; + + OSMO_STRBUF_PRINTF(sb, " '%s'=", get_value_string(iei_strs, ie_coding->tag)); + if (multi_count_p) + OSMO_STRBUF_PRINTF(sb, "{ "); + + for (i = 0; i < n; i++) { + unsigned int memb_ofs; + + /* If this is a repeated IE, encode from the correct array index */ + if (multi_count_p && i >= ie_coding->count_max) + return -ENOTSUP; + if (i > 0) + OSMO_STRBUF_PRINTF(sb, ", "); + + memb_ofs = ie_coding->memb_ofs + i * ie_coding->memb_array_pitch; + + if (ie_coding->nested_ies) { + OSMO_STRBUF_PRINTF(sb, "{"); + OSMO_STRBUF_APPEND(sb, osmo_gtlvs_encode_to_str_buf, decoded_struct, obj_ofs + memb_ofs, + ie_coding->nested_ies, iei_strs); + OSMO_STRBUF_PRINTF(sb, " }"); + } else { + if (ie_coding->enc_to_str_func) + OSMO_STRBUF_APPEND(sb, ie_coding->enc_to_str_func, MEMB(obj, memb_ofs)); + else + OSMO_STRBUF_PRINTF(sb, "(enc_to_str_func==NULL)"); + } + } + + if (multi_count_p) + OSMO_STRBUF_PRINTF(sb, " }"); + } + return sb.chars_needed; +} + +/*! Compose a human readable string describing a decoded struct. + * Like osmo_gtlvs_encode_to_str_buf() but returns a talloc allocated string. + * \param[in] ctx talloc context to allocate from, e.g. OTC_SELECT. + * \param[in] decoded_struct C struct data to encode. + * \param[in] obj_ofs Nesting offset, pass as 0. + * \param[in] ie_coding A {} terminated list of IEI tags to encode (if present) and instructions for encoding. + * \param[in] iei_strs value_string array to give IEI names in tag headers, or NULL. + * \return human readable string. + */ +char *osmo_gtlvs_encode_to_str_c(void *ctx, const void *decoded_struct, unsigned int obj_ofs, + const struct osmo_gtlv_coding *ie_coding, const struct value_string *iei_strs) +{ + OSMO_NAME_C_IMPL(ctx, 256, "ERROR", osmo_gtlvs_encode_to_str_buf, decoded_struct, obj_ofs, ie_coding, iei_strs) +} -- cgit v1.2.3