From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- src/VBox/Runtime/common/asn1/asn1-dump.cpp | 644 +++++++++++++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 src/VBox/Runtime/common/asn1/asn1-dump.cpp (limited to 'src/VBox/Runtime/common/asn1/asn1-dump.cpp') diff --git a/src/VBox/Runtime/common/asn1/asn1-dump.cpp b/src/VBox/Runtime/common/asn1/asn1-dump.cpp new file mode 100644 index 00000000..e75d379e --- /dev/null +++ b/src/VBox/Runtime/common/asn1/asn1-dump.cpp @@ -0,0 +1,644 @@ +/* $Id: asn1-dump.cpp $ */ +/** @file + * IPRT - ASN.1, Structure Dumper. + */ + +/* + * Copyright (C) 2006-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * 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, in version 3 of the + * License. + * + * 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 . + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "internal/iprt.h" +#include + +#include +#include +#ifdef IN_RING3 +# include +#endif +#include + +#include + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Dump data structure. + */ +typedef struct RTASN1DUMPDATA +{ + /** RTASN1DUMP_F_XXX. */ + uint32_t fFlags; + /** The printfv like output function. */ + PFNRTDUMPPRINTFV pfnPrintfV; + /** PrintfV user argument. */ + void *pvUser; +} RTASN1DUMPDATA; +/** Pointer to a dump data structure. */ +typedef RTASN1DUMPDATA *PRTASN1DUMPDATA; + + +#ifndef IN_SUP_HARDENED_R3 + +/* + * Since we're the only user of OIDs, this stuff lives here. + * Should that ever change, this code needs to move elsewhere and get it's own public API. + */ +# include "oiddb.h" + + +/** + * Searches a range in the big table for a key. + * + * @returns Pointer to the matching entry. NULL if not found. + * @param iEntry The start of the range. + * @param cEntries The number of entries in the range. + * @param uKey The key to find. + */ +DECLINLINE(PCRTOIDENTRYBIG) rtOidDbLookupBig(uint32_t iEntry, uint32_t cEntries, uint32_t uKey) +{ + /* Not worth doing binary search here, too few entries. */ + while (cEntries-- > 0) + { + uint32_t const uThisKey = g_aBigOidTable[iEntry].uKey; + if (uThisKey >= uKey) + { + if (uThisKey == uKey) + return &g_aBigOidTable[iEntry]; + break; + } + iEntry++; + } + return NULL; +} + + +/** + * Searches a range in the small table for a key. + * + * @returns Pointer to the matching entry. NULL if not found. + * @param iEntry The start of the range. + * @param cEntries The number of entries in the range. + * @param uKey The key to find. + */ +DECLINLINE(PCRTOIDENTRYSMALL) rtOidDbLookupSmall(uint32_t iEntry, uint32_t cEntries, uint32_t uKey) +{ + if (cEntries < 6) + { + /* Linear search for small ranges. */ + while (cEntries-- > 0) + { + uint32_t const uThisKey = g_aSmallOidTable[iEntry].uKey; + if (uThisKey >= uKey) + { + if (uThisKey == uKey) + return &g_aSmallOidTable[iEntry]; + break; + } + iEntry++; + } + } + else + { + /* Binary search. */ + uint32_t iEnd = iEntry + cEntries; + for (;;) + { + uint32_t const i = iEntry + (iEnd - iEntry) / 2; + uint32_t const uThisKey = g_aSmallOidTable[i].uKey; + if (uThisKey < uKey) + { + iEntry = i + 1; + if (iEntry >= iEnd) + break; + } + else if (uThisKey > uKey) + { + iEnd = i; + if (iEnd <= iEntry) + break; + } + else + return &g_aSmallOidTable[i]; + } + } + return NULL; +} + + +/** + * Queries the name for an object identifier. + * + * @returns IPRT status code (VINF_SUCCESS, VERR_NOT_FOUND, + * VERR_BUFFER_OVERFLOW) + * @param pauComponents The components making up the object ID. + * @param cComponents The number of components. + * @param pszDst Where to store the name if found. + * @param cbDst The size of the destination buffer. + */ +static int rtOidDbQueryObjIdName(uint32_t const *pauComponents, uint8_t cComponents, char *pszDst, size_t cbDst) +{ + int rc = VERR_NOT_FOUND; + if (cComponents > 0) + { + /* + * The top level is always in the small table as the range is restricted to 0,1,2. + */ + bool fBigTable = false; + uint32_t cEntries = RT_MIN(RT_ELEMENTS(g_aSmallOidTable), 3); + uint32_t iEntry = 0; + for (;;) + { + uint32_t const uKey = *pauComponents++; + if (!fBigTable) + { + PCRTOIDENTRYSMALL pSmallHit = rtOidDbLookupSmall(iEntry, cEntries, uKey); + if (pSmallHit) + { + if (--cComponents == 0) + { + if (RTBldProgStrTabQueryString(&g_OidDbStrTab, pSmallHit->offString, + pSmallHit->cchString, pszDst, cbDst) >= 0) + return VINF_SUCCESS; + rc = VERR_BUFFER_OVERFLOW; + break; + } + cEntries = pSmallHit->cChildren; + if (cEntries) + { + iEntry = pSmallHit->idxChildren; + fBigTable = pSmallHit->fBigTable; + continue; + } + } + } + else + { + PCRTOIDENTRYBIG pBigHit = rtOidDbLookupBig(iEntry, cEntries, uKey); + if (pBigHit) + { + if (--cComponents == 0) + { + if (RTBldProgStrTabQueryString(&g_OidDbStrTab, pBigHit->offString, + pBigHit->cchString, pszDst, cbDst) >= 0) + return VINF_SUCCESS; + rc = VERR_BUFFER_OVERFLOW; + break; + } + cEntries = pBigHit->cChildren; + if (cEntries) + { + iEntry = pBigHit->idxChildren; + fBigTable = pBigHit->fBigTable; + continue; + } + } + } + break; + } + } + + return rc; +} + + +/** + * Queries the name for an object identifier. + * + * This API is simple and more or less requires a + * + * @returns IPRT status code. + * @retval VINF_SUCCESS on success. + * @retval VERR_NOT_FOUND if not found. + * @retval VERR_BUFFER_OVERFLOW if more buffer space is required. + * + * @param pauComponents The components making up the object ID. + * @param cComponents The number of components. + * @param pszDst Where to store the name if found. + * @param cbDst The size of the destination buffer. + */ +RTDECL(int) RTAsn1QueryObjIdName(PCRTASN1OBJID pObjId, char *pszDst, size_t cbDst) +{ + return rtOidDbQueryObjIdName(pObjId->pauComponents, pObjId->cComponents, pszDst, cbDst); +} + +#endif /* !IN_SUP_HARDENED_R3 */ + + + +/** + * Wrapper around FNRTASN1DUMPPRINTFV. + * + * @param pData The dump data structure. + * @param pszFormat Format string. + * @param ... Format arguments. + */ +static void rtAsn1DumpPrintf(PRTASN1DUMPDATA pData, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + pData->pfnPrintfV(pData->pvUser, pszFormat, va); + va_end(va); +} + + +/** + * Prints indentation. + * + * @param pData The dump data structure. + * @param uDepth The indentation depth. + */ +static void rtAsn1DumpPrintIdent(PRTASN1DUMPDATA pData, uint32_t uDepth) +{ + uint32_t cchLeft = uDepth * 2; + while (cchLeft > 0) + { + static char const s_szSpaces[] = " "; + uint32_t cch = RT_MIN(cchLeft, sizeof(s_szSpaces) - 1); + rtAsn1DumpPrintf(pData, &s_szSpaces[sizeof(s_szSpaces) - 1 - cch]); + cchLeft -= cch; + } +} + + +/** + * Dumps UTC TIME and GENERALIZED TIME + * + * @param pData The dump data structure. + * @param pAsn1Core The ASN.1 core object representation. + * @param pszType The time type name. + */ +static void rtAsn1DumpTime(PRTASN1DUMPDATA pData, PCRTASN1CORE pAsn1Core, const char *pszType) +{ + if ((pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT)) + { + PCRTASN1TIME pTime = (PCRTASN1TIME)pAsn1Core; + rtAsn1DumpPrintf(pData, "%s -- %04u-%02u-%02u %02u:%02u:%02.%09Z\n", + pszType, + pTime->Time.i32Year, pTime->Time.u8Month, pTime->Time.u8MonthDay, + pTime->Time.u8Hour, pTime->Time.u8Minute, pTime->Time.u8Second, + pTime->Time.u32Nanosecond); + } + else if (pAsn1Core->cb > 0 && pAsn1Core->cb < 32 && pAsn1Core->uData.pch) + rtAsn1DumpPrintf(pData, "%s '%.*s'\n", pszType, (size_t)pAsn1Core->cb, pAsn1Core->uData.pch); + else + rtAsn1DumpPrintf(pData, "%s -- cb=%u\n", pszType, pAsn1Core->cb); +} + + +/** + * Dumps strings sharing the RTASN1STRING structure. + * + * @param pData The dump data structure. + * @param pAsn1Core The ASN.1 core object representation. + * @param pszType The string type name. + * @param uDepth The current identation level. + */ +static void rtAsn1DumpString(PRTASN1DUMPDATA pData, PCRTASN1CORE pAsn1Core, const char *pszType, uint32_t uDepth) +{ + rtAsn1DumpPrintf(pData, "%s", pszType); + + const char *pszPostfix = "'\n"; + bool fUtf8 = false; + const char *pch = pAsn1Core->uData.pch; + uint32_t cch = pAsn1Core->cb; + PCRTASN1STRING pString = (PCRTASN1STRING)pAsn1Core; + if ( (pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT) + && pString->pszUtf8 + && pString->cchUtf8) + { + fUtf8 = true; + pszPostfix = "' -- utf-8\n"; + } + + if (cch == 0 || !pch) + rtAsn1DumpPrintf(pData, "-- cb=%u\n", pszType, pAsn1Core->cb); + else + { + if (cch >= 48) + { + rtAsn1DumpPrintf(pData, "\n"); + rtAsn1DumpPrintIdent(pData, uDepth + 1); + } + rtAsn1DumpPrintf(pData, " '"); + + /** @todo Handle BMP and UNIVERSIAL strings specially. */ + do + { + const char *pchStart = pch; + while ( cch > 0 + && (uint8_t)*pch >= 0x20 + && (!fUtf8 ? (uint8_t)*pch < 0x7f : (uint8_t)*pch != 0x7f) + && *pch != '\'') + cch--, pch++; + if (pchStart != pch) + rtAsn1DumpPrintf(pData, "%.*s", pch - pchStart, pchStart); + + while ( cch > 0 + && ( (uint8_t)*pch < 0x20 + || (!fUtf8 ? (uint8_t)*pch >= 0x7f : (uint8_t)*pch == 0x7f) + || (uint8_t)*pch == '\'') ) + { + rtAsn1DumpPrintf(pData, "\\x%02x", *pch); + cch--; + pch++; + } + } while (cch > 0); + + rtAsn1DumpPrintf(pData, pszPostfix); + } +} + + +/** + * Dumps the type and value of an universal ASN.1 type. + * + * @returns True if it opens a child, false if not. + * @param pData The dumper data. + * @param pAsn1Core The ASN.1 object to dump. + * @param uDepth The current depth (for indentation). + */ +static bool rtAsn1DumpUniversalTypeAndValue(PRTASN1DUMPDATA pData, PCRTASN1CORE pAsn1Core, uint32_t uDepth) +{ + const char *pszValuePrefix = "-- value:"; + const char *pszDefault = ""; + if (pAsn1Core->fFlags & RTASN1CORE_F_DEFAULT) + { + pszValuePrefix = "DEFAULT"; + pszDefault = "DEFAULT "; + } + + bool fOpen = false; + switch (pAsn1Core->uRealTag) + { + case ASN1_TAG_BOOLEAN: + if (pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT) + rtAsn1DumpPrintf(pData, "BOOLEAN %s %RTbool\n", pszValuePrefix, ((PCRTASN1BOOLEAN)pAsn1Core)->fValue); + else if (pAsn1Core->cb == 1 && pAsn1Core->uData.pu8) + rtAsn1DumpPrintf(pData, "BOOLEAN %s %u\n", pszValuePrefix, *pAsn1Core->uData.pu8); + else + rtAsn1DumpPrintf(pData, "BOOLEAN -- cb=%u\n", pAsn1Core->cb); + break; + + case ASN1_TAG_INTEGER: + if ((pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT) && pAsn1Core->cb <= 8) + rtAsn1DumpPrintf(pData, "INTEGER %s %llu / %#llx\n", pszValuePrefix, + ((PCRTASN1INTEGER)pAsn1Core)->uValue, ((PCRTASN1INTEGER)pAsn1Core)->uValue); + else if (pAsn1Core->cb == 0 || pAsn1Core->cb >= 512 || !pAsn1Core->uData.pu8) + rtAsn1DumpPrintf(pData, "INTEGER -- cb=%u\n", pAsn1Core->cb); + else if (pAsn1Core->cb <= 32) + rtAsn1DumpPrintf(pData, "INTEGER %s %.*Rhxs\n", pszValuePrefix, (size_t)pAsn1Core->cb, pAsn1Core->uData.pu8); + else + rtAsn1DumpPrintf(pData, "INTEGER %s\n%.*Rhxd\n", pszValuePrefix, (size_t)pAsn1Core->cb, pAsn1Core->uData.pu8); + break; + + case ASN1_TAG_BIT_STRING: + if ((pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT)) + { + PCRTASN1BITSTRING pBitString = (PCRTASN1BITSTRING)pAsn1Core; + rtAsn1DumpPrintf(pData, "BIT STRING %s-- cb=%u cBits=%#x cMaxBits=%#x", + pszDefault, pBitString->Asn1Core.cb, pBitString->cBits, pBitString->cMaxBits); + if (pBitString->cBits <= 64) + rtAsn1DumpPrintf(pData, " value=%#llx\n", RTAsn1BitString_GetAsUInt64(pBitString)); + else + rtAsn1DumpPrintf(pData, "\n"); + } + else + rtAsn1DumpPrintf(pData, "BIT STRING %s-- cb=%u\n", pszDefault, pAsn1Core->cb); + fOpen = pAsn1Core->pOps != NULL; + break; + + case ASN1_TAG_OCTET_STRING: + rtAsn1DumpPrintf(pData, "OCTET STRING %s-- cb=%u\n", pszDefault, pAsn1Core->cb); + fOpen = pAsn1Core->pOps != NULL; + break; + + case ASN1_TAG_NULL: + rtAsn1DumpPrintf(pData, "NULL\n"); + break; + + case ASN1_TAG_OID: + if ((pAsn1Core->fFlags & RTASN1CORE_F_PRIMITE_TAG_STRUCT)) + { +#ifndef IN_SUP_HARDENED_R3 + PCRTASN1OBJID pObjId = (PCRTASN1OBJID)pAsn1Core; + char szName[64]; + if (rtOidDbQueryObjIdName(pObjId->pauComponents, pObjId->cComponents, szName, sizeof(szName)) == VINF_SUCCESS) + rtAsn1DumpPrintf(pData, "OBJECT IDENTIFIER %s%s ('%s')\n", + pszDefault, szName, ((PCRTASN1OBJID)pAsn1Core)->szObjId); + else +#endif + rtAsn1DumpPrintf(pData, "OBJECT IDENTIFIER %s'%s'\n", pszDefault, ((PCRTASN1OBJID)pAsn1Core)->szObjId); + } + else + rtAsn1DumpPrintf(pData, "OBJECT IDENTIFIER %s -- cb=%u\n", pszDefault, pAsn1Core->cb); + break; + + case ASN1_TAG_OBJECT_DESCRIPTOR: + rtAsn1DumpPrintf(pData, "OBJECT DESCRIPTOR -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_EXTERNAL: + rtAsn1DumpPrintf(pData, "EXTERNAL -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_REAL: + rtAsn1DumpPrintf(pData, "REAL -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_ENUMERATED: + rtAsn1DumpPrintf(pData, "ENUMERATED -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_EMBEDDED_PDV: + rtAsn1DumpPrintf(pData, "EMBEDDED PDV -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_UTF8_STRING: + rtAsn1DumpString(pData, pAsn1Core, "UTF8 STRING", uDepth); + break; + + case ASN1_TAG_RELATIVE_OID: + rtAsn1DumpPrintf(pData, "RELATIVE OBJECT IDENTIFIER -- cb=%u TODO\n", pAsn1Core->cb); + break; + + case ASN1_TAG_SEQUENCE: + rtAsn1DumpPrintf(pData, "SEQUENCE -- cb=%u\n", pAsn1Core->cb); + fOpen = true; + break; + case ASN1_TAG_SET: + rtAsn1DumpPrintf(pData, "SET -- cb=%u\n", pAsn1Core->cb); + fOpen = true; + break; + + case ASN1_TAG_NUMERIC_STRING: + rtAsn1DumpString(pData, pAsn1Core, "NUMERIC STRING", uDepth); + break; + + case ASN1_TAG_PRINTABLE_STRING: + rtAsn1DumpString(pData, pAsn1Core, "PRINTABLE STRING", uDepth); + break; + + case ASN1_TAG_T61_STRING: + rtAsn1DumpString(pData, pAsn1Core, "T61 STRING", uDepth); + break; + + case ASN1_TAG_VIDEOTEX_STRING: + rtAsn1DumpString(pData, pAsn1Core, "VIDEOTEX STRING", uDepth); + break; + + case ASN1_TAG_IA5_STRING: + rtAsn1DumpString(pData, pAsn1Core, "IA5 STRING", uDepth); + break; + + case ASN1_TAG_GRAPHIC_STRING: + rtAsn1DumpString(pData, pAsn1Core, "GRAPHIC STRING", uDepth); + break; + + case ASN1_TAG_VISIBLE_STRING: + rtAsn1DumpString(pData, pAsn1Core, "VISIBLE STRING", uDepth); + break; + + case ASN1_TAG_GENERAL_STRING: + rtAsn1DumpString(pData, pAsn1Core, "GENERAL STRING", uDepth); + break; + + case ASN1_TAG_UNIVERSAL_STRING: + rtAsn1DumpString(pData, pAsn1Core, "UNIVERSAL STRING", uDepth); + break; + + case ASN1_TAG_BMP_STRING: + rtAsn1DumpString(pData, pAsn1Core, "BMP STRING", uDepth); + break; + + case ASN1_TAG_UTC_TIME: + rtAsn1DumpTime(pData, pAsn1Core, "UTC TIME"); + break; + + case ASN1_TAG_GENERALIZED_TIME: + rtAsn1DumpTime(pData, pAsn1Core, "GENERALIZED TIME"); + break; + + case ASN1_TAG_CHARACTER_STRING: + rtAsn1DumpPrintf(pData, "CHARACTER STRING -- cb=%u TODO\n", pAsn1Core->cb); + break; + + default: + rtAsn1DumpPrintf(pData, "[UNIVERSAL %u]\n", pAsn1Core->uTag); + break; + } + return fOpen; +} + + +/** @callback_method_impl{FNRTASN1ENUMCALLBACK} */ +static DECLCALLBACK(int) rtAsn1DumpEnumCallback(PRTASN1CORE pAsn1Core, const char *pszName, uint32_t uDepth, void *pvUser) +{ + PRTASN1DUMPDATA pData = (PRTASN1DUMPDATA)pvUser; + if (!pAsn1Core->fFlags) + return VINF_SUCCESS; + + bool fOpen = false; + rtAsn1DumpPrintIdent(pData, uDepth); + switch (pAsn1Core->fClass & ASN1_TAGCLASS_MASK) + { + case ASN1_TAGCLASS_UNIVERSAL: + rtAsn1DumpPrintf(pData, "%-16s ", pszName); + fOpen = rtAsn1DumpUniversalTypeAndValue(pData, pAsn1Core, uDepth); + break; + + case ASN1_TAGCLASS_CONTEXT: + if ((pAsn1Core->fRealClass & ASN1_TAGCLASS_MASK) == ASN1_TAGCLASS_UNIVERSAL) + { + rtAsn1DumpPrintf(pData, "%-16s [%u] ", pszName, pAsn1Core->uTag); + fOpen = rtAsn1DumpUniversalTypeAndValue(pData, pAsn1Core, uDepth); + } + else + { + rtAsn1DumpPrintf(pData, "%-16s [%u]\n", pszName, pAsn1Core->uTag); + fOpen = true; + } + break; + + case ASN1_TAGCLASS_APPLICATION: + if ((pAsn1Core->fRealClass & ASN1_TAGCLASS_MASK) == ASN1_TAGCLASS_UNIVERSAL) + { + rtAsn1DumpPrintf(pData, "%-16s [APPLICATION %u] ", pszName, pAsn1Core->uTag); + fOpen = rtAsn1DumpUniversalTypeAndValue(pData, pAsn1Core, uDepth); + } + else + { + rtAsn1DumpPrintf(pData, "%-16s [APPLICATION %u]\n", pszName, pAsn1Core->uTag); + fOpen = true; + } + break; + + case ASN1_TAGCLASS_PRIVATE: + if (RTASN1CORE_IS_DUMMY(pAsn1Core)) + rtAsn1DumpPrintf(pData, "%-16s DUMMY\n", pszName); + else + { + rtAsn1DumpPrintf(pData, "%-16s [PRIVATE %u]\n", pszName, pAsn1Core->uTag); + fOpen = true; + } + break; + } + /** @todo {} */ + + /* + * Recurse. + */ + if ( pAsn1Core->pOps + && pAsn1Core->pOps->pfnEnum) + pAsn1Core->pOps->pfnEnum(pAsn1Core, rtAsn1DumpEnumCallback, uDepth, pData); + return VINF_SUCCESS; +} + + +RTDECL(int) RTAsn1Dump(PCRTASN1CORE pAsn1Core, uint32_t fFlags, uint32_t uLevel, PFNRTDUMPPRINTFV pfnPrintfV, void *pvUser) +{ + if ( pAsn1Core->pOps + && pAsn1Core->pOps->pfnEnum) + { + RTASN1DUMPDATA Data; + Data.fFlags = fFlags; + Data.pfnPrintfV = pfnPrintfV; + Data.pvUser = pvUser; + + return pAsn1Core->pOps->pfnEnum((PRTASN1CORE)pAsn1Core, rtAsn1DumpEnumCallback, uLevel, &Data); + } + return VINF_SUCCESS; +} + -- cgit v1.2.3