/* * (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 #define RETURN_ERROR(RC, TAG_INST, FMT, ARGS...) \ do {\ if (err_cb) { \ if ((TAG_INST).instance_present) \ err_cb(err_cb_data, (void *)decoded_struct, __FILE__, __LINE__, \ "tag 0x%x = %s instance %u: " FMT " (%d: %s)\n", \ (TAG_INST).tag, get_value_string(iei_strs, (TAG_INST).tag), \ (TAG_INST).instance, ##ARGS, \ RC, strerror((RC) > 0 ? (RC) : -(RC))); \ else \ err_cb(err_cb_data, (void *)decoded_struct, __FILE__, __LINE__, \ "tag 0x%x = %s: " FMT " (%d: %s)\n", \ (TAG_INST).tag, get_value_string(iei_strs, (TAG_INST).tag), ##ARGS, \ RC, strerror((RC) > 0 ? (RC) : -(RC))); \ } \ return RC; \ } while (0) /* Reverse offsetof(): return the address of the struct member for a given parent struct and member offset value. * \param parent Pointer to the parent struct, e.g. decoded_struct as passed to osmo_gtlvs_decode(). * \param parent_size sizeof(*parent), for memory bounds checking, e.g. decoded_struct_size. * \param memb_ofs byte offset to reach a member (e.g. osmo_gtlv_coding.memb_ofs, .presence_flag_ofs, ...). */ static const void *membof_const(const void *parent, size_t parent_size, unsigned int memb_ofs) { const uint8_t *p = parent; const uint8_t *end = p + parent_size; const uint8_t *memb = p + memb_ofs; OSMO_ASSERT(memb < end); OSMO_ASSERT(memb >= p); return memb; } static void *membof(void *parent, size_t parent_size, unsigned int memb_ofs) { return (void *)membof_const((const void *)parent, parent_size, memb_ofs); } /*! 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, size_t decoded_struct_size, 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 = membof(decoded_struct, decoded_struct_size, obj_ofs); size_t obj_maxlen = decoded_struct_size - obj_ofs; const struct osmo_gtlv_coding *iec; unsigned int *multi_count_p = NULL; /* 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->ti, \ "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->ti, "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; /* Unordered: for every tag, look for matching IE definitions from the start. */ iec = ie_coding; /* There may be multiple subsequent occurences of the same tag, to be decoded into multiple members of * the decoded struct. Iterate until encountering a still "free slot" for decoding. */ do { /* Find the IE coding for this tag */ for (; !osmo_gtlv_coding_end(iec) && osmo_gtlv_tag_inst_cmp(&iec->ti, >lv->ti); iec++); /* No such IE coding found. */ if (osmo_gtlv_coding_end(iec)) break; /* Keep track how often this tag can occur in total */ 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 ? membof(obj, obj_maxlen, iec->presence_flag_ofs) : NULL; multi_count_p = iec->has_count ? membof(obj, obj_maxlen, iec->count_ofs) : NULL; if ((presence_flag_p && *presence_flag_p) || (multi_count_p && *multi_count_p >= iec->count_max)) { iec++; 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[]. */ if (!presence_flag_p && !multi_count_p) { CHECK_SEEN(iec); if (*seen_p) { iec++; continue; } } /* Found an IE coding that has not yet been decoded / still has unused struct members to decode * to. */ break; } while (1); if (osmo_gtlv_coding_end(iec)) { if (ie_max_allowed_count) { /* This tag exists in the protocol definitions, but we've run out of struct members to * decode the tag to. */ RETURN_ERROR(-ENOTSUP, gtlv->ti, "Only %u instances of this IE are supported per message", ie_max_allowed_count); } /* This is an unknown tag, no such IEI 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, decoded_struct_size, obj_ofs + memb_ofs, &inner_tlv, ordered, iec->nested_ies, err_cb, err_cb_data, iei_strs); if (rc) RETURN_ERROR(rc, gtlv->ti, "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->ti, "IE definition lacks a dec_func()"); rc = iec->dec_func(decoded_struct, membof(obj, obj_maxlen, memb_ofs), gtlv); if (rc) RETURN_ERROR(rc, gtlv->ti, "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 ? membof(obj, obj_maxlen, iec->count_ofs) : NULL; if (multi_count_p) { if (*multi_count_p < iec->count_mandatory) RETURN_ERROR(-EINVAL, iec->ti, "%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->ti, "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, size_t decoded_struct_size, 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 = membof(decoded_struct, decoded_struct_size, obj_ofs); size_t obj_maxlen = decoded_struct_size - obj_ofs; osmo_gtlv_load_start(gtlv); /* Ordered: traverse the ie_coding exactly once, expecting tags to be decoded to appear in the same order. */ for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { int rc; bool *presence_flag; unsigned int *multi_count; struct osmo_gtlv_tag_inst peek_ti; #define UPDATE_STATE_FROM_IE_CODING() do { \ presence_flag = ie_coding->has_presence_flag ? \ membof(obj, obj_maxlen, ie_coding->presence_flag_ofs) : NULL; \ multi_count = ie_coding->has_count ? \ membof(obj, obj_maxlen, ie_coding->count_ofs) : NULL; \ } while (0) UPDATE_STATE_FROM_IE_CODING(); rc = osmo_gtlv_load_next_by_tag_inst(gtlv, &ie_coding->ti); switch (rc) { case 0: break; case -ENOENT: if (!presence_flag && (!multi_count || *multi_count < ie_coding->count_mandatory)) RETURN_ERROR(rc, ie_coding->ti, "Missing mandatory IE"); if (presence_flag) *presence_flag = false; continue; default: RETURN_ERROR(rc, ie_coding->ti, "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; unsigned int memb_ofs; #define UPDATE_MEMB_IDX() do { \ memb_next_array_idx = multi_count ? *multi_count : 0; \ memb_ofs = ie_coding->memb_ofs + memb_next_array_idx * ie_coding->memb_array_pitch; \ } while (0) UPDATE_MEMB_IDX(); if (multi_count && memb_next_array_idx >= ie_coding->count_max) { /* This repeated IE is full. But the protocol may define this same IEI to follow again, * with a different target struct member. See if this IEI appears again. */ const struct osmo_gtlv_coding *iec = ie_coding; bool iei_appears_again = false; for (iec = ie_coding + 1; !osmo_gtlv_coding_end(iec); iec++) { if (osmo_gtlv_tag_inst_cmp(>lv->ti, &iec->ti) == 0) { iei_appears_again = true; break; } } if (!iei_appears_again) RETURN_ERROR(-ENOTSUP, ie_coding->ti, "Only %u instances of this IE are supported per message", ie_coding->count_max); /* IEI does appear again. Skip forward to this ie_coding, and use that to decode the * current TLV (do not call osmo_gtlv_load_next_by_tag_inst() again, or we'd skip the * TLV in the message being decoded). */ ie_coding = iec; UPDATE_STATE_FROM_IE_CODING(); UPDATE_MEMB_IDX(); } /* 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, decoded_struct_size, 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->ti, "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->ti, "IE definition lacks a dec_func()"); rc = ie_coding->dec_func(decoded_struct, membof(obj, obj_maxlen, memb_ofs), gtlv); if (rc) RETURN_ERROR(rc, ie_coding->ti, "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, &peek_ti) || osmo_gtlv_tag_inst_cmp(&peek_ti, >lv->ti)) { /* 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, size_t decoded_struct_size, 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, decoded_struct_size, obj_ofs, gtlv, ie_coding, err_cb, err_cb_data, iei_strs); else return osmo_gtlvs_decode_unordered(decoded_struct, decoded_struct_size, 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, size_t decoded_struct_size, 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) { const void *obj = membof_const(decoded_struct, decoded_struct_size, obj_ofs); size_t obj_maxlen = decoded_struct_size - obj_ofs; if (!ie_coding) return -ENOTSUP; for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { int rc; const bool *presence_flag_p = ie_coding->has_presence_flag ? membof_const(obj, obj_maxlen, ie_coding->presence_flag_ofs) : NULL; const unsigned int *multi_count_p = ie_coding->has_count ? membof_const(obj, obj_maxlen, 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->ti, "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_tli(gtlv, &ie_coding->ti, 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->ti, "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, decoded_struct_size, obj_ofs + memb_ofs, ie_coding->nested_ies, err_cb, err_cb_data, iei_strs); if (rc) RETURN_ERROR(rc, ie_coding->ti, "Error while encoding TLV structure nested inside this IE"); } else { rc = ie_coding->enc_func(gtlv, decoded_struct, membof_const(obj, obj_maxlen, memb_ofs)); if (rc) RETURN_ERROR(rc, ie_coding->ti, "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, size_t decoded_struct_size, 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 }; const void *obj = membof_const(decoded_struct, decoded_struct_size, obj_ofs); size_t obj_maxlen = decoded_struct_size - obj_ofs; if (!ie_coding) return -ENOTSUP; for (; !osmo_gtlv_coding_end(ie_coding); ie_coding++) { const bool *presence_flag_p = ie_coding->has_presence_flag ? membof_const(obj, obj_maxlen, ie_coding->presence_flag_ofs) : NULL; const unsigned int *multi_count_p = ie_coding->has_count ? membof_const(obj, obj_maxlen, 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->ti.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, decoded_struct_size, 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, membof_const(obj, obj_maxlen, 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, size_t decoded_struct_size, 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, decoded_struct_size, obj_ofs, ie_coding, iei_strs) }