Changeset 99392 in vbox for trunk/src/VBox
- Timestamp:
- Apr 13, 2023 4:48:07 PM (23 months ago)
- svn:sync-xref-src-repo-rev:
- 156830
- Location:
- trunk/src/VBox/Main
- Files:
-
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/VBox/Main/include/GuestCtrlImplPrivate.h
r99262 r99392 973 973 974 974 GuestToolboxStreamValue(void) { } 975 GuestToolboxStreamValue(const char *pszValue )976 : mValue(pszValue ) {}975 GuestToolboxStreamValue(const char *pszValue, size_t cwcValue = RTSTR_MAX) 976 : mValue(pszValue, cwcValue) {} 977 977 978 978 GuestToolboxStreamValue(const GuestToolboxStreamValue& aThat) … … 996 996 typedef std::map < Utf8Str, GuestToolboxStreamValue >::const_iterator GuestCtrlStreamPairMapIterConst; 997 997 998 class GuestToolboxStream; 999 998 1000 /** 999 1001 * Class representing a block of stream pairs (key=value). Each block in a raw guest … … 1001 1003 * end of a guest stream is marked by "\0\0\0\0". 1002 1004 * 1005 * An empty stream block will be treated as being incomplete. 1006 * 1003 1007 * Only used for the busybox-like toolbox commands within VBoxService. 1004 1008 * Deprecated, do not use anymore. … … 1006 1010 class GuestToolboxStreamBlock 1007 1011 { 1012 friend GuestToolboxStream; 1013 1008 1014 public: 1009 1015 … … 1029 1035 int32_t GetInt32(const char *pszKey, int32_t iDefault = 0) const; 1030 1036 1031 bool IsEmpty(void) { return mPairs.empty(); } 1032 1037 bool IsComplete(void) const { return !m_mapPairs.empty() && m_fComplete; } 1038 bool IsEmpty(void) const { return m_mapPairs.empty(); } 1039 1040 int SetValueEx(const char *pszKey, size_t cwcKey, const char *pszValue, size_t cwcValue, bool fOverwrite = false); 1033 1041 int SetValue(const char *pszKey, const char *pszValue); 1034 1042 1035 1043 protected: 1036 1044 1037 GuestCtrlStreamPairMap mPairs; 1045 /** Wheter the stream block is marked as complete. 1046 * An empty stream block is considered as incomplete. */ 1047 bool m_fComplete; 1048 /** Map of stream pairs this block contains.*/ 1049 GuestCtrlStreamPairMap m_mapPairs; 1038 1050 }; 1039 1051 … … 1043 1055 typedef std::vector< GuestToolboxStreamBlock >::const_iterator GuestCtrlStreamObjectsIterConst; 1044 1056 1057 /** Defines a single terminator as a single char. */ 1058 #define GUESTTOOLBOX_STRM_TERM '\0' 1059 /** Defines a single terminator as a string. */ 1060 #define GUESTTOOLBOX_STRM_TERM_STR "\0" 1061 /** Defines the termination sequence for a single key/value pair. */ 1062 #define GUESTTOOLBOX_STRM_TERM_PAIR_STR GUESTTOOLBOX_STRM_TERM_STR 1063 /** Defines the termination sequence for a single stream block. */ 1064 #define GUESTTOOLBOX_STRM_TERM_BLOCK_STR GUESTTOOLBOX_STRM_TERM_STR GUESTTOOLBOX_STRM_TERM_STR 1065 /** Defines the termination sequence for the stream. */ 1066 #define GUESTTOOLBOX_STRM_TERM_STREAM_STR GUESTTOOLBOX_STRM_TERM_STR GUESTTOOLBOX_STRM_TERM_STR GUESTTOOLBOX_STRM_TERM_STR GUESTTOOLBOX_STRM_TERM_STR 1067 /** Defines how many consequtive terminators a key/value pair has. */ 1068 #define GUESTTOOLBOX_STRM_PAIR_TERM_CNT 1 1069 /** Defines how many consequtive terminators a stream block has. */ 1070 #define GUESTTOOLBOX_STRM_BLK_TERM_CNT 2 1071 /** Defines how many consequtive terminators a stream has. */ 1072 #define GUESTTOOLBOX_STRM_TERM_CNT 4 1073 1045 1074 /** 1046 1075 * Class for parsing machine-readable guest process output by VBoxService' … … 1068 1097 #endif 1069 1098 1070 size_t GetOffset() { return m_offBuffer; } 1071 1072 size_t GetSize() { return m_cbUsed; } 1099 size_t GetOffset(void) const { return m_offBuf; } 1100 1101 size_t GetSize(void) const { return m_cbUsed; } 1102 1103 size_t GetBlocks(void) const { return m_cBlocks; } 1073 1104 1074 1105 int ParseBlock(GuestToolboxStreamBlock &streamBlock); … … 1084 1115 size_t m_cbUsed; 1085 1116 /** Current byte offset within the internal stream buffer. */ 1086 size_t m_offBuf fer;1117 size_t m_offBuf; 1087 1118 /** Internal stream buffer. */ 1088 1119 BYTE *m_pbBuffer; 1120 /** How many completed stream blocks already were processed. */ 1121 size_t m_cBlocks; 1089 1122 }; 1090 1123 -
trunk/src/VBox/Main/src-client/GuestCtrlPrivate.cpp
r99011 r99392 535 535 536 536 GuestToolboxStreamBlock::GuestToolboxStreamBlock(void) 537 { 538 539 } 537 : m_fComplete(false) { } 540 538 541 539 GuestToolboxStreamBlock::~GuestToolboxStreamBlock() … … 549 547 void GuestToolboxStreamBlock::Clear(void) 550 548 { 551 mPairs.clear(); 549 m_fComplete = false; 550 m_mapPairs.clear(); 552 551 } 553 552 … … 558 557 void GuestToolboxStreamBlock::DumpToLog(void) const 559 558 { 560 LogFlowFunc(("Dumping contents of stream block=0x%p (%ld items ):\n",561 this, m Pairs.size()));562 563 for (GuestCtrlStreamPairMapIterConst it = m Pairs.begin();564 it != m Pairs.end(); ++it)559 LogFlowFunc(("Dumping contents of stream block=0x%p (%ld items, fComplete=%RTbool):\n", 560 this, m_mapPairs.size(), m_fComplete)); 561 562 for (GuestCtrlStreamPairMapIterConst it = m_mapPairs.begin(); 563 it != m_mapPairs.end(); ++it) 565 564 { 566 565 LogFlowFunc(("\t%s=%s\n", it->first.c_str(), it->second.mValue.c_str())); … … 610 609 size_t GuestToolboxStreamBlock::GetCount(void) const 611 610 { 612 return m Pairs.size();611 return m_mapPairs.size(); 613 612 } 614 613 … … 645 644 try 646 645 { 647 GuestCtrlStreamPairMapIterConst itPairs = m Pairs.find(pszKey);648 if (itPairs != m Pairs.end())646 GuestCtrlStreamPairMapIterConst itPairs = m_mapPairs.find(pszKey); 647 if (itPairs != m_mapPairs.end()) 649 648 return itPairs->second.mValue.c_str(); 650 649 } … … 711 710 712 711 /** 712 * Sets a value to a key or deletes a key by setting a NULL value. Extended version. 713 * 714 * @return VBox status code. 715 * @param pszKey Key name to process. 716 * @param cwcKey Maximum characters of \a pszKey to process. 717 * @param pszValue Value to set. Set NULL for deleting the key. 718 * @param cwcValue Maximum characters of \a pszValue to process. 719 * @param fOverwrite Whether a key can be overwritten with a new value if it already exists. Will assert otherwise. 720 */ 721 int GuestToolboxStreamBlock::SetValueEx(const char *pszKey, size_t cwcKey, const char *pszValue, size_t cwcValue, 722 bool fOverwrite /* = false */) 723 { 724 AssertPtrReturn(pszKey, VERR_INVALID_POINTER); 725 AssertReturn(cwcKey, VERR_INVALID_PARAMETER); 726 727 int vrc = VINF_SUCCESS; 728 try 729 { 730 Utf8Str const strKey(pszKey, cwcKey); 731 732 /* Take a shortcut and prevent crashes on some funny versions 733 * of STL if map is empty initially. */ 734 if (!m_mapPairs.empty()) 735 { 736 GuestCtrlStreamPairMapIter it = m_mapPairs.find(strKey); 737 if (it != m_mapPairs.end()) 738 { 739 if (pszValue == NULL) 740 m_mapPairs.erase(it); 741 else if (!fOverwrite) 742 AssertMsgFailedReturn(("Key '%*s' already exists! Value is '%s'\n", cwcKey, pszKey, m_mapPairs[strKey].mValue.c_str()), 743 VERR_ALREADY_EXISTS); 744 } 745 } 746 747 if (pszValue) 748 { 749 GuestToolboxStreamValue val(pszValue, cwcValue); 750 Log3Func(("strKey='%s', strValue='%s'\n", strKey.c_str(), val.mValue.c_str())); 751 m_mapPairs[strKey] = val; 752 } 753 } 754 catch (const std::exception &) 755 { 756 /** @todo set vrc? */ 757 } 758 return vrc; 759 } 760 761 /** 713 762 * Sets a value to a key or deletes a key by setting a NULL value. 714 763 * … … 719 768 int GuestToolboxStreamBlock::SetValue(const char *pszKey, const char *pszValue) 720 769 { 721 AssertPtrReturn(pszKey, VERR_INVALID_POINTER); 722 723 int vrc = VINF_SUCCESS; 724 try 725 { 726 Utf8Str const strKey(pszKey); 727 728 /* Take a shortcut and prevent crashes on some funny versions 729 * of STL if map is empty initially. */ 730 if (!mPairs.empty()) 731 { 732 GuestCtrlStreamPairMapIter it = mPairs.find(strKey); 733 if (it != mPairs.end()) 734 mPairs.erase(it); 735 } 736 737 if (pszValue) 738 { 739 GuestToolboxStreamValue val(pszValue); 740 mPairs[strKey] = val; 741 } 742 } 743 catch (const std::exception &) 744 { 745 /** @todo set vrc? */ 746 } 747 return vrc; 770 return SetValueEx(pszKey, RTSTR_MAX, pszValue, RTSTR_MAX); 748 771 } 749 772 … … 754 777 , m_cbAllocated(0) 755 778 , m_cbUsed(0) 756 , m_offBuffer(0) 757 , m_pbBuffer(NULL) { } 779 , m_offBuf(0) 780 , m_pbBuffer(NULL) 781 , m_cBlocks(0) { } 758 782 759 783 GuestToolboxStream::~GuestToolboxStream(void) … … 778 802 779 803 /* Rewind the buffer if it's empty. */ 780 size_t cbInBuf = m_cbUsed - m_offBuf fer;804 size_t cbInBuf = m_cbUsed - m_offBuf; 781 805 bool const fAddToSet = cbInBuf == 0; 782 806 if (fAddToSet) 783 m_cbUsed = m_offBuf fer= 0;807 m_cbUsed = m_offBuf = 0; 784 808 785 809 /* Try and see if we can simply append the data. */ … … 792 816 { 793 817 /* Move any buffered data to the front. */ 794 cbInBuf = m_cbUsed - m_offBuf fer;818 cbInBuf = m_cbUsed - m_offBuf; 795 819 if (cbInBuf == 0) 796 m_cbUsed = m_offBuf fer= 0;797 else if (m_offBuf fer) /* Do we have something to move? */798 { 799 memmove(m_pbBuffer, &m_pbBuffer[m_offBuf fer], cbInBuf);820 m_cbUsed = m_offBuf = 0; 821 else if (m_offBuf) /* Do we have something to move? */ 822 { 823 memmove(m_pbBuffer, &m_pbBuffer[m_offBuf], cbInBuf); 800 824 m_cbUsed = cbInBuf; 801 m_offBuf fer= 0;825 m_offBuf = 0; 802 826 } 803 827 … … 851 875 m_cbAllocated = 0; 852 876 m_cbUsed = 0; 853 m_offBuffer = 0; 877 m_offBuf = 0; 878 m_cBlocks = 0; 854 879 } 855 880 … … 864 889 { 865 890 LogFlowFunc(("Dumping contents of stream=0x%p (cbAlloc=%u, cbSize=%u, cbOff=%u) to %s\n", 866 m_pbBuffer, m_cbAllocated, m_cbUsed, m_offBuf fer, pszFile));891 m_pbBuffer, m_cbAllocated, m_cbUsed, m_offBuf, pszFile)); 867 892 868 893 RTFILE hFile; … … 874 899 } 875 900 } 876 #endif 877 878 /** 879 * Tries to parse the next upcoming pair block within the internal 880 * buffer.881 * 882 * Returns VERR_NO_DATA is no data is in internal buffer or buffer has been883 * completely parsed already.884 * 885 * Returns VERR_MORE_DATA if current block was parsed (with zero or more pairs886 * stored in stream block) but still contains incomplete (unterminated)887 * data.888 * 889 * Returns VINF_SUCCESS if current block was parsed until the next upcoming890 * block (with zero or more pairs stored in stream block).901 #endif /* DEBUG */ 902 903 /** 904 * Tries to parse the next upcoming pair block within the internal buffer. 905 * 906 * Parsing behavior: 907 * - A stream can contain one or multiple blocks and is terminated by four (4) "\0". 908 * - A block (or "object") contains one or multiple key=value pairs and is terminated with two (2) "\0". 909 * - Each key=value pair is terminated by a single (1) "\0". 910 * 911 * As new data can arrive at a later time eventually completing a pair / block / stream, 912 * the algorithm needs to be careful not intepreting its current data too early. So only skip termination 913 * sequences if we really know that the termination sequence is complete. See comments down below. 914 * 915 * No locking done. 891 916 * 892 917 * @return VBox status code. 893 * @param streamBlock Reference to guest stream block to fill. 918 * @retval VINF_EOF if the stream reached its end. 919 * @param streamBlock Reference to guest stream block to fill 894 920 */ 895 921 int GuestToolboxStream::ParseBlock(GuestToolboxStreamBlock &streamBlock) 896 922 { 923 AssertMsgReturn(streamBlock.m_fComplete == false, ("Block object already marked as being completed\n"), VERR_WRONG_ORDER); 924 897 925 if ( !m_pbBuffer 898 926 || !m_cbUsed) 899 return VERR_NO_DATA; 900 901 AssertReturn(m_offBuffer <= m_cbUsed, VERR_INVALID_PARAMETER); 902 if (m_offBuffer == m_cbUsed) 903 return VERR_NO_DATA; 904 905 int vrc = VINF_SUCCESS; 906 char * const pszOff = (char *)&m_pbBuffer[m_offBuffer]; 907 size_t cbLeft = m_offBuffer < m_cbUsed ? m_cbUsed - m_offBuffer : 0; 908 char *pszStart = pszOff; 909 while (cbLeft > 0 && *pszStart != '\0') 910 { 911 char * const pszPairEnd = RTStrEnd(pszStart, cbLeft); 912 if (!pszPairEnd) 913 { 914 vrc = VERR_MORE_DATA; 915 break; 916 } 917 size_t const cchPair = (size_t)(pszPairEnd - pszStart); 918 char *pszSep = (char *)memchr(pszStart, '=', cchPair); 919 if (pszSep) 920 *pszSep = '\0'; /* Terminate the separator so that we can use pszStart as our key from now on. */ 921 else 922 { 923 vrc = VERR_MORE_DATA; /** @todo r=bird: This is BOGUS because we'll be stuck here if the guest feeds us bad data! */ 924 break; 925 } 926 char const * const pszVal = pszSep + 1; 927 928 vrc = streamBlock.SetValue(pszStart, pszVal); 927 return VINF_EOF; 928 929 AssertReturn(m_offBuf <= m_cbUsed, VERR_INVALID_PARAMETER); 930 if (m_offBuf == m_cbUsed) 931 return VINF_EOF; 932 933 char * const pszStart = (char *)&m_pbBuffer[m_offBuf]; 934 935 size_t cbLeftParsed = m_offBuf < m_cbUsed ? m_cbUsed - m_offBuf : 0; 936 size_t cbLeftLookAhead = cbLeftParsed; 937 938 char *pszLookAhead = pszStart; /* Look ahead pointer to count terminators. */ 939 char *pszParsed = pszStart; /* Points to data considered as being parsed already. */ 940 941 Log4Func(("Current @ %zu/%zu:\n%.*RhXd\n", m_offBuf, m_cbUsed, RT_MIN(cbLeftParsed, _1K), pszStart)); 942 943 size_t cTerm = 0; 944 945 /* 946 * We have to be careful when handling single terminators ('\0') here, as we might not know yet 947 * if it's part of a multi-terminator seqeuence. 948 * 949 * So handle and skip those *only* when we hit a non-terminator char again. 950 */ 951 int vrc = VINF_SUCCESS; 952 while (cbLeftLookAhead) 953 { 954 /* Count consequtive terminators. */ 955 if (*pszLookAhead == GUESTTOOLBOX_STRM_TERM) 956 { 957 cTerm++; 958 pszLookAhead++; 959 cbLeftLookAhead--; 960 continue; 961 } 962 963 pszParsed = pszLookAhead; 964 cbLeftParsed = cbLeftLookAhead; 965 966 /* We hit a non-terminator (again); now interpret where we are, and 967 * bail out if we need to. */ 968 if (cTerm >= 2) 969 { 970 Log2Func(("Hit end of termination sequence (%zu)\n", cTerm)); 971 break; 972 } 973 974 cTerm = 0; /* Reset consequtive counter. */ 975 976 char * const pszPairEnd = RTStrEnd(pszParsed, cbLeftParsed); 977 if (!pszPairEnd) /* No zero terminator found (yet), try next time. */ 978 break; 979 980 Log3Func(("Pair '%s' (%u)\n", pszParsed, strlen(pszParsed))); 981 982 Assert(pszPairEnd != pszParsed); 983 size_t const cbPair = (size_t)(pszPairEnd - pszParsed); 984 Assert(cbPair); 985 const char *pszSep = (const char *)memchr(pszParsed, '=', cbPair); 986 if (!pszSep) /* No separator found (yet), try next time. */ 987 break; 988 989 /* Skip the separator so that pszSep points to the actual value. */ 990 pszSep++; 991 992 char const * const pszKey = pszParsed; 993 char const * const pszVal = pszSep; 994 995 vrc = streamBlock.SetValueEx(pszKey, pszSep - pszKey - 1, pszVal, pszPairEnd - pszVal); 929 996 if (RT_FAILURE(vrc)) 930 997 return vrc; 931 998 932 /* Next pair. */ 933 pszStart = pszPairEnd + 1; 934 cbLeft -= cchPair + 1; 935 } 936 937 /* If we did not do any movement but we have stuff left 938 * in our buffer just skip the current termination so that 939 * we can try next time. */ 940 size_t cbDistance = (pszStart - pszOff); 941 if ( !cbDistance 942 && cbLeft > 0 943 && *pszStart == '\0' 944 && m_offBuffer < m_cbUsed) 945 cbDistance++; 946 m_offBuffer += cbDistance; 999 if (cbPair >= cbLeftParsed) 1000 break; 1001 1002 /* Accounting for next iteration. */ 1003 pszParsed = pszPairEnd; 1004 Assert(cbLeftParsed >= cbPair); 1005 cbLeftParsed -= cbPair; 1006 1007 pszLookAhead = pszPairEnd; 1008 cbLeftLookAhead = cbLeftParsed; 1009 1010 if (cbLeftParsed) 1011 Log4Func(("Next iteration @ %zu:\n%.*RhXd\n", pszParsed - pszStart, cbLeftParsed, pszParsed)); 1012 } 1013 1014 if (cbLeftParsed) 1015 Log4Func(("Done @ %zu:\n%.*RhXd\n", pszParsed - pszStart, cbLeftParsed, pszParsed)); 1016 1017 m_offBuf += pszParsed - pszStart; /* Only account really parsed content. */ 1018 Assert(m_offBuf <= m_cbUsed); 1019 1020 /* Did we hit a block or stream termination sequence? */ 1021 if (cTerm >= GUESTTOOLBOX_STRM_BLK_TERM_CNT) 1022 { 1023 if (!streamBlock.IsEmpty()) /* Only account and complete blocks which have values in it. */ 1024 { 1025 m_cBlocks++; 1026 streamBlock.m_fComplete = true; 1027 #ifdef DEBUG 1028 streamBlock.DumpToLog(); 1029 #endif 1030 } 1031 1032 if (cTerm >= GUESTTOOLBOX_STRM_TERM_CNT) 1033 { 1034 m_offBuf = m_cbUsed; 1035 vrc = VINF_EOF; 1036 } 1037 } 1038 1039 LogFlowThisFunc(("cbLeft=%zu, offBuffer=%zu / cbUsed=%zu, cBlocks=%zu, cTerm=%zu -> current block has %RU64 pairs (complete = %RTbool), rc=%Rrc\n", 1040 cbLeftParsed, m_offBuf, m_cbUsed, m_cBlocks, cTerm, streamBlock.GetCount(), streamBlock.IsComplete(), vrc)); 947 1041 948 1042 return vrc; -
trunk/src/VBox/Main/src-client/GuestProcessImpl.cpp
r99120 r99392 2352 2352 * 2353 2353 * @returns VBox status code. 2354 * @retval VINF_EOF if the stream reached its end. 2354 2355 * @param uHandle Guest process file handle to get current block for. 2355 2356 * @param strmBlock Where to return the stream block on success. … … 2357 2358 int GuestProcessToolbox::getCurrentBlock(uint32_t uHandle, GuestToolboxStreamBlock &strmBlock) 2358 2359 { 2359 constGuestToolboxStream *pStream = NULL;2360 GuestToolboxStream *pStream = NULL; 2360 2361 if (uHandle == GUEST_PROC_OUT_H_STDOUT) 2361 2362 pStream = &mStdOut; … … 2366 2367 return VERR_INVALID_PARAMETER; 2367 2368 2368 /** @todo Why not using pStream down below and hardcode to mStdOut? */ 2369 2370 int vrc; 2371 do 2372 { 2373 /* Try parsing the data to see if the current block is complete. */ 2374 vrc = mStdOut.ParseBlock(strmBlock); 2375 if (strmBlock.GetCount()) 2376 break; 2377 } while (RT_SUCCESS(vrc)); 2378 2379 LogFlowThisFunc(("vrc=%Rrc, %RU64 pairs\n", vrc, strmBlock.GetCount())); 2369 int vrc = pStream->ParseBlock(strmBlock); 2370 2371 LogFlowThisFunc(("vrc=%Rrc, currently %RU64 pairs\n", vrc, strmBlock.GetCount())); 2380 2372 return vrc; 2381 2373 } … … 2594 2586 LogFlowThisFunc(("fToolWaitFlags=0x%x, pStreamBlock=%p, pvrcGuest=%p\n", fToolWaitFlags, pStrmBlkOut, pvrcGuest)); 2595 2587 2596 /* Can we parse the next block without waiting? */2597 2588 int vrc; 2589 2590 /* Is the next block complete without waiting for new data from the guest? */ 2598 2591 if (fToolWaitFlags & GUESTPROCESSTOOL_WAIT_FLAG_STDOUT_BLOCK) 2599 2592 { 2600 2593 AssertPtr(pStrmBlkOut); 2601 2594 vrc = getCurrentBlock(GUEST_PROC_OUT_H_STDOUT, *pStrmBlkOut); 2602 if (RT_SUCCESS(vrc)) 2595 if ( RT_SUCCESS(vrc) 2596 && pStrmBlkOut->IsComplete()) 2603 2597 return vrc; 2604 2598 /* else do the waiting below. */ -
trunk/src/VBox/Main/testcase/tstGuestCtrlParseBuffer.cpp
r98526 r99392 1 1 /* $Id$ */ 2 2 /** @file 3 * Output stream parsing test cases.3 * Tests for VBoxService toolbox output streams. 4 4 */ 5 5 … … 39 39 40 40 #include <iprt/env.h> 41 #include <iprt/file.h> 41 42 #include <iprt/test.h> 43 #include <iprt/rand.h> 42 44 #include <iprt/stream.h> 43 45 … … 50 52 * Defined Constants And Macros * 51 53 *********************************************************************************************************************************/ 52 #define STR_SIZE(a_sz) RT_STR_TUPLE(a_sz) 54 /** Defines a test entry string size (in bytes). */ 55 #define TST_STR_BYTES(a_sz) (sizeof(a_sz) - 1) 56 /** Defines a test entry string, followed by its size (in bytes). */ 57 #define TST_STR_AND_BYTES(a_sz) a_sz, (sizeof(a_sz) - 1) 58 /** Defines the termination sequence for a single key/value pair. */ 59 #define TST_STR_VAL_TRM GUESTTOOLBOX_STRM_TERM_PAIR_STR 60 /** Defines the termination sequence for a single stream block. */ 61 #define TST_STR_BLK_TRM GUESTTOOLBOX_STRM_TERM_BLOCK_STR 62 /** Defines the termination sequence for the stream. */ 63 #define TST_STR_STM_TRM GUESTTOOLBOX_STRM_TERM_STREAM_STR 53 64 54 65 … … 71 82 char szUnterm2[] = { 'f', 'o', 'o', '3', '=', 'b', 'a', 'r', '3' }; 72 83 84 PRTLOGGER g_pLog = NULL; 85 86 /** 87 * Tests single block parsing. 88 */ 73 89 static struct 74 90 { … … 81 97 } g_aTestBlocks[] = 82 98 { 83 /*84 * Single object parsing.85 * An object is represented by one or multiple key=value pairs which are86 * separated by a single "\0". If this termination is missing it will be assumed87 * that we need to collect more data to do a successful parsing.88 */89 99 /* Invalid stuff. */ 90 { NULL, 0, 0, 0, 0, VERR_INVALID_POINTER }, 91 { NULL, 512, 0, 0, 0, VERR_INVALID_POINTER }, 92 { "", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 93 { "", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 94 { "foo=bar1", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 95 { "foo=bar2", 0, 50, 50, 0, VERR_INVALID_PARAMETER }, 96 /* Empty buffers. */ 97 { "", 1, 0, 1, 0, VINF_SUCCESS }, 98 { "\0", 1, 0, 1, 0, VINF_SUCCESS }, 99 /* Unterminated values (missing "\0"). */ 100 { STR_SIZE("test1"), 0, 0, 0, VERR_MORE_DATA }, 101 { STR_SIZE("test2="), 0, 0, 0, VERR_MORE_DATA }, 102 { STR_SIZE("test3=test3"), 0, 0, 0, VERR_MORE_DATA }, 103 { STR_SIZE("test4=test4\0t41"), 0, sizeof("test4=test4\0") - 1, 1, VERR_MORE_DATA }, 104 { STR_SIZE("test5=test5\0t51=t51"), 0, sizeof("test5=test5\0") - 1, 1, VERR_MORE_DATA }, 105 /* Next block unterminated. */ 106 { STR_SIZE("t51=t51\0t52=t52\0\0t53=t53"), 0, sizeof("t51=t51\0t52=t52\0") - 1, 2, VINF_SUCCESS }, 107 { STR_SIZE("test6=test6\0\0t61=t61"), 0, sizeof("test6=test6\0") - 1, 1, VINF_SUCCESS }, 108 /* Good stuff. */ 109 { STR_SIZE("test61=\0test611=test611\0"), 0, sizeof("test61=\0test611=test611\0") - 1, 2, VINF_SUCCESS }, 110 { STR_SIZE("test7=test7\0\0"), 0, sizeof("test7=test7\0") - 1, 1, VINF_SUCCESS }, 111 { STR_SIZE("test8=test8\0t81=t81\0\0"), 0, sizeof("test8=test8\0t81=t81\0") - 1, 2, VINF_SUCCESS }, 100 { NULL, 0, 0, 0, 0, VERR_INVALID_POINTER }, 101 { NULL, 512, 0, 0, 0, VERR_INVALID_POINTER }, 102 { "", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 103 { "", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 104 { "foo=bar1", 0, 0, 0, 0, VERR_INVALID_PARAMETER }, 105 { "foo=bar2", 0, 50, 50, 0, VERR_INVALID_PARAMETER }, 106 /* Has a empty key (not allowed). */ 107 { TST_STR_AND_BYTES("=test2" TST_STR_VAL_TRM), 0, TST_STR_BYTES(""), 0, VERR_INVALID_PARAMETER }, 108 /* Empty buffers, i.e. nothing to process. */ 109 /* Index 6*/ 110 { "", 1, 0, 0, 0, VINF_SUCCESS }, 111 { TST_STR_VAL_TRM, 1, 0, 0, 0, VINF_SUCCESS }, 112 /* Stream termination sequence. */ 113 { TST_STR_AND_BYTES(TST_STR_STM_TRM), 0, 114 TST_STR_BYTES (TST_STR_STM_TRM), 0, VINF_EOF }, 115 /* Trash after stream termination sequence (skipped / ignored). */ 116 { TST_STR_AND_BYTES(TST_STR_STM_TRM "trash"), 0, 117 TST_STR_BYTES (TST_STR_STM_TRM "trash"), 0, VINF_EOF }, 118 { TST_STR_AND_BYTES("a=b" TST_STR_STM_TRM), 0, 119 TST_STR_BYTES ("a=b" TST_STR_STM_TRM), 1, VINF_EOF }, 120 { TST_STR_AND_BYTES("a=b" TST_STR_VAL_TRM "c=d" TST_STR_STM_TRM), 0, 121 TST_STR_BYTES ("a=b" TST_STR_VAL_TRM "c=d" TST_STR_STM_TRM), 2, VINF_EOF }, 122 /* Unterminated values (missing separator, i.e. no valid pair). */ 123 { TST_STR_AND_BYTES("test1"), 0, 0, 0, VINF_SUCCESS }, 124 /* Has a NULL value (allowed). */ 125 { TST_STR_AND_BYTES("test2=" TST_STR_VAL_TRM), 0, 126 TST_STR_BYTES ("test2="), 1, VINF_SUCCESS }, 127 /* One completed pair only. */ 128 { TST_STR_AND_BYTES("test3=test3" TST_STR_VAL_TRM), 0, 129 TST_STR_BYTES ("test3=test3"), 1, VINF_SUCCESS }, 130 /* One completed pair, plus an unfinished pair (separator + terminator missing). */ 131 { TST_STR_AND_BYTES("test4=test4" TST_STR_VAL_TRM "t41"), 0, 132 TST_STR_BYTES ("test4=test4" TST_STR_VAL_TRM), 1, VINF_SUCCESS }, 133 /* Two completed pairs. */ 134 { TST_STR_AND_BYTES("test5=test5" TST_STR_VAL_TRM "t51=t51" TST_STR_VAL_TRM), 0, 135 TST_STR_BYTES ("test5=test5" TST_STR_VAL_TRM "t51=t51"), 2, VINF_SUCCESS }, 136 /* One complete block, next block unterminated. */ 137 { TST_STR_AND_BYTES("a51=b51" TST_STR_VAL_TRM "c52=d52" TST_STR_BLK_TRM "e53=f53"), 0, 138 TST_STR_BYTES ("a51=b51" TST_STR_VAL_TRM "c52=d52" TST_STR_BLK_TRM), 2, VINF_SUCCESS }, 139 /* Ditto. */ 140 { TST_STR_AND_BYTES("test6=test6" TST_STR_BLK_TRM "t61=t61"), 0, 141 TST_STR_BYTES ("test6=test6" TST_STR_BLK_TRM), 1, VINF_SUCCESS }, 142 /* Two complete pairs with a complete stream. */ 143 { TST_STR_AND_BYTES("test61=" TST_STR_VAL_TRM "test611=test612" TST_STR_STM_TRM), 0, 144 TST_STR_BYTES ("test61=" TST_STR_VAL_TRM "test611=test612" TST_STR_STM_TRM), 2, VINF_EOF }, 145 /* One complete block. */ 146 { TST_STR_AND_BYTES("test7=test7" TST_STR_BLK_TRM), 0, 147 TST_STR_BYTES ("test7=test7"), 1, VINF_SUCCESS }, 148 /* Ditto. */ 149 { TST_STR_AND_BYTES("test81=test82" TST_STR_VAL_TRM "t81=t82" TST_STR_BLK_TRM), 0, 150 TST_STR_BYTES ("test81=test82" TST_STR_VAL_TRM "t81=t82"), 2, VINF_SUCCESS }, 112 151 /* Good stuff, but with a second block -- should be *not* taken into account since 113 152 * we're only interested in parsing/handling the first object. */ 114 { STR_SIZE("t9=t9\0t91=t91\0\0t92=t92\0\0"), 0, sizeof("t9=t9\0t91=t91\0") - 1, 2, VINF_SUCCESS }, 153 { TST_STR_AND_BYTES("t91=t92" TST_STR_VAL_TRM "t93=t94" TST_STR_BLK_TRM "t95=t96" TST_STR_BLK_TRM), 0, 154 TST_STR_BYTES ("t91=t92" TST_STR_VAL_TRM "t93=t94" TST_STR_BLK_TRM), 2, VINF_SUCCESS }, 115 155 /* Nasty stuff. */ 116 156 /* iso 8859-1 encoding (?) of 'aou' all with diaeresis '=f' and 'ao' with diaeresis. */ 117 { STR_SIZE("\xe4\xf6\xfc=\x66\xe4\xf6\0\0"), 0, sizeof("\xe4\xf6\xfc=\x66\xe4\xf6\0") - 1, 1, VINF_SUCCESS }, 157 { TST_STR_AND_BYTES("1\xe4\xf6\xfc=\x66\xe4\xf6" TST_STR_BLK_TRM), 0, 158 TST_STR_BYTES ("1\xe4\xf6\xfc=\x66\xe4\xf6"), 1, VINF_SUCCESS }, 118 159 /* Like above, but after the first '\0' it adds 'ooo=aaa' all letters with diaeresis. */ 119 { STR_SIZE("\xe4\xf6\xfc=\x66\xe4\xf6\0\xf6\xf6\xf6=\xe4\xe4\xe4"),120 0, sizeof("\xe4\xf6\xfc=\x66\xe4\xf6\0") - 1, 1, VERR_MORE_DATA},121 /* Some "real world" examples . */122 { STR_SIZE("hdr_id=vbt_stat\0hdr_ver=1\0name=foo.txt\0\0"), 0, sizeof("hdr_id=vbt_stat\0hdr_ver=1\0name=foo.txt\0") - 1,123 160 { TST_STR_AND_BYTES("2\xe4\xf6\xfc=\x66\xe4\xf6" TST_STR_VAL_TRM "\xf6\xf6\xf6=\xe4\xe4\xe4"), 0, 161 TST_STR_BYTES ("2\xe4\xf6\xfc=\x66\xe4\xf6" TST_STR_VAL_TRM), 1, VINF_SUCCESS }, 162 /* Some "real world" examples from VBoxService toolbox. */ 163 { TST_STR_AND_BYTES("hdr_id=vbt_stat" TST_STR_VAL_TRM "hdr_ver=1" TST_STR_VAL_TRM "name=foo.txt" TST_STR_BLK_TRM), 0, 164 TST_STR_BYTES ("hdr_id=vbt_stat" TST_STR_VAL_TRM "hdr_ver=1" TST_STR_VAL_TRM "name=foo.txt"), 3, VINF_SUCCESS } 124 165 }; 125 166 167 /** 168 * Tests parsing multiple stream blocks. 169 * 170 * Same parsing behavior as for the tests above apply. 171 */ 126 172 static struct 127 173 { 174 /** Stream data. */ 128 175 const char *pbData; 176 /** Size of stream data (in bytes). */ 129 177 size_t cbData; 130 178 /** Number of data blocks retrieved. These are separated by "\0\0". */ … … 135 183 { 136 184 /* No blocks. */ 137 { "\0\0\0\0", sizeof("\0\0\0\0"), 0, VERR_NO_DATA }, 185 { "", sizeof(""), 0, VINF_SUCCESS }, 186 /* Empty block (no key/value pairs), will not be accounted. */ 187 { TST_STR_STM_TRM, 188 TST_STR_BYTES(TST_STR_STM_TRM), 0, VINF_EOF }, 138 189 /* Good stuff. */ 139 { "\0b1=b1\0\0", sizeof("\0b1=b1\0\0"), 1, VERR_NO_DATA},140 { "b1=b1\0\0", sizeof("b1=b1\0\0"), 1, VERR_NO_DATA},141 { "b1=b1\0b2=b2\0\0", sizeof("b1=b1\0b2=b2\0\0"), 1, VERR_NO_DATA},142 { "b1=b1\0b2=b2\0\0\0", sizeof("b1=b1\0b2=b2\0\0\0"), 1, VERR_NO_DATA}190 { TST_STR_AND_BYTES(TST_STR_VAL_TRM "b1=b2" TST_STR_STM_TRM), 1, VINF_EOF }, 191 { TST_STR_AND_BYTES("b3=b31" TST_STR_STM_TRM), 1, VINF_EOF }, 192 { TST_STR_AND_BYTES("b4=b41" TST_STR_BLK_TRM "b51=b61" TST_STR_STM_TRM), 2, VINF_EOF }, 193 { TST_STR_AND_BYTES("b5=b51" TST_STR_VAL_TRM "b61=b71" TST_STR_STM_TRM), 1, VINF_EOF } 143 194 }; 144 195 145 int manualTest(void) 146 { 147 int rc = VINF_SUCCESS; 148 static struct 196 /** 197 * Reads and parses the stream from a given file. 198 * 199 * @returns RTEXITCODE 200 * @param pszFile Absolute path to file to parse. 201 */ 202 static int tstReadFromFile(const char *pszFile) 203 { 204 RTFILE fh; 205 int rc = RTFileOpen(&fh, pszFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE); 206 AssertRCReturn(rc, RTEXITCODE_FAILURE); 207 208 size_t cbFileSize; 209 rc = RTFileQuerySize(fh, &cbFileSize); 210 AssertRCReturn(rc, RTEXITCODE_FAILURE); 211 212 GuestToolboxStream stream; 213 GuestToolboxStreamBlock block; 214 215 size_t cPairs = 0; 216 size_t cBlocks = 0; 217 218 unsigned aToRead[] = { 256, 23, 13 }; 219 unsigned i = 0; 220 221 size_t cbToRead = cbFileSize; 222 223 for (unsigned a = 0; a < 32; a++) 149 224 { 150 const char *pbData; 151 size_t cbData; 152 uint32_t offStart; 153 uint32_t offAfter; 154 uint32_t cMapElements; 155 int iResult; 156 } const s_aTest[] = 157 { 158 { "test5=test5\0t51=t51", sizeof("test5=test5\0t51=t51"), 0, sizeof("test5=test5\0") - 1, 1, VERR_MORE_DATA }, 159 { "\0\0test5=test5\0t51=t51", sizeof("\0\0test5=test5\0t51=t51"), 0, sizeof("\0\0test5=test5\0") - 1, 1, VERR_MORE_DATA }, 160 }; 161 162 for (unsigned iTest = 0; iTest < RT_ELEMENTS(s_aTest); iTest++) 163 { 164 RTTestIPrintf(RTTESTLVL_DEBUG, "Manual test #%d\n", iTest); 165 166 GuestToolboxStream stream; 167 rc = stream.AddData((BYTE *)s_aTest[iTest].pbData, s_aTest[iTest].cbData); 168 169 for (;;) 225 uint8_t buf[_64K]; 226 do 170 227 { 171 GuestToolboxStreamBlock block; 228 size_t cbChunk = RT_MIN(cbToRead, i < RT_ELEMENTS(aToRead) ? aToRead[i++] : RTRandU64Ex(8, RT_MIN(sizeof(buf), 64))); 229 if (cbChunk > cbToRead) 230 cbChunk = cbToRead; 231 if (cbChunk) 232 { 233 RTTestIPrintf(RTTESTLVL_DEBUG, "Reading %zu bytes (of %zu left) ...\n", cbChunk, cbToRead); 234 235 size_t cbRead; 236 rc = RTFileRead(fh, &buf, cbChunk, &cbRead); 237 AssertRCBreak(rc); 238 239 if (!cbRead) 240 continue; 241 242 cbToRead -= cbRead; 243 244 rc = stream.AddData((BYTE *)buf, cbRead); 245 AssertRCBreak(rc); 246 } 247 172 248 rc = stream.ParseBlock(block); 173 RTTestIPrintf(RTTESTLVL_DEBUG, "\tReturned with rc=%Rrc, numItems=%ld\n", 174 rc, block.GetCount()); 175 176 if (block.GetCount()) 177 break; 178 } 249 Assert(rc != VERR_INVALID_PARAMETER); 250 RTTestIPrintf(RTTESTLVL_DEBUG, "Parsing ended with %Rrc\n", rc); 251 if (block.IsComplete()) 252 { 253 /* Sanity checks; disable this if you parse anything else but fsinfo output from VBoxService toolbox. */ 254 //Assert(block.GetString("name") != NULL); 255 256 cPairs += block.GetCount(); 257 cBlocks = stream.GetBlocks(); 258 block.Clear(); 259 } 260 } while (VINF_SUCCESS == rc /* Might also be VINF_EOF when finished */); 261 262 RTTestIPrintf(RTTESTLVL_ALWAYS, "Total %zu blocks + %zu pairs\n", cBlocks, cPairs); 263 264 /* Reset. */ 265 RTFileSeek(fh, 0, RTFILE_SEEK_BEGIN, NULL); 266 cbToRead = cbFileSize; 267 cPairs = 0; 268 cBlocks = 0; 269 block.Clear(); 270 stream.Destroy(); 179 271 } 180 272 181 return rc; 273 int rc2 = RTFileClose(fh); 274 if (RT_SUCCESS(rc)) 275 rc = rc2; 276 277 return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; 182 278 } 183 279 184 int main( )280 int main(int argc, char **argv) 185 281 { 186 282 RTTEST hTest; … … 189 285 return rcExit; 190 286 RTTestBanner(hTest); 287 288 #ifdef DEBUG 289 RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME_PROG; 290 #if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) 291 fFlags |= RTLOGFLAGS_USECRLF; 292 #endif 293 static const char * const s_apszLogGroups[] = VBOX_LOGGROUP_NAMES; 294 int rc = RTLogCreate(&g_pLog, fFlags, "guest_control.e.l.l2.l3.f", NULL, 295 RT_ELEMENTS(s_apszLogGroups), s_apszLogGroups, RTLOGDEST_STDOUT, NULL /*"vkat-release.log"*/); 296 AssertRCReturn(rc, rc); 297 RTLogSetDefaultInstance(g_pLog); 298 #endif 299 300 if (argc > 1) 301 return tstReadFromFile(argv[1]); 191 302 192 303 RTTestIPrintf(RTTESTLVL_DEBUG, "Initializing COM...\n"); … … 198 309 } 199 310 200 #ifdef DEBUG_andy 201 int rc = manualTest(); 202 if (RT_FAILURE(rc)) 203 return RTEXITCODE_FAILURE; 204 #endif 205 206 AssertCompile(sizeof("sizecheck") == 10); 207 AssertCompile(sizeof("off=rab") == 8); 208 AssertCompile(sizeof("off=rab\0\0") == 10); 209 210 RTTestSub(hTest, "Lines"); 311 AssertCompile(TST_STR_BYTES("1") == 1); 312 AssertCompile(TST_STR_BYTES("sizecheck") == 9); 313 AssertCompile(TST_STR_BYTES("off=rab") == 7); 314 AssertCompile(TST_STR_BYTES("off=rab\0\0") == 9); 315 316 RTTestSub(hTest, "Blocks"); 317 318 RTTestDisableAssertions(hTest); 319 211 320 for (unsigned iTest = 0; iTest < RT_ELEMENTS(g_aTestBlocks); iTest++) 212 321 { 213 RTTestIPrintf(RTTESTLVL_DEBUG, "=> Test #%u\n", iTest);322 RTTestIPrintf(RTTESTLVL_DEBUG, "=> Block test #%u:\n'%.*RhXd\n", iTest, g_aTestBlocks[iTest].cbData, g_aTestBlocks[iTest].pbData); 214 323 215 324 GuestToolboxStream stream; 216 if (RT_FAILURE(g_aTestBlocks[iTest].iResult))217 RTTestDisableAssertions(hTest);218 325 int iResult = stream.AddData((BYTE *)g_aTestBlocks[iTest].pbData, g_aTestBlocks[iTest].cbData); 219 if (RT_FAILURE(g_aTestBlocks[iTest].iResult))220 RTTestRestoreAssertions(hTest);221 326 if (RT_SUCCESS(iResult)) 222 327 { … … 224 329 iResult = stream.ParseBlock(curBlock); 225 330 if (iResult != g_aTestBlocks[iTest].iResult) 226 RTTestFailed(hTest, "Block #%u: Returned %Rrc, expected %Rrc ", iTest, iResult, g_aTestBlocks[iTest].iResult);331 RTTestFailed(hTest, "Block #%u: Returned %Rrc, expected %Rrc\n", iTest, iResult, g_aTestBlocks[iTest].iResult); 227 332 else if (stream.GetOffset() != g_aTestBlocks[iTest].offAfter) 228 RTTestFailed(hTest, "Block #%uOffset %zu wrong, expected %u\n", 229 iTest, stream.GetOffset(), g_aTestBlocks[iTest].offAfter); 333 RTTestFailed(hTest, "Block #%u: Offset %zu wrong ('%#x'), expected %u ('%#x')\n", 334 iTest, stream.GetOffset(), g_aTestBlocks[iTest].pbData[stream.GetOffset()], 335 g_aTestBlocks[iTest].offAfter, g_aTestBlocks[iTest].pbData[g_aTestBlocks[iTest].offAfter]); 230 336 else if (iResult == VERR_MORE_DATA) 231 337 RTTestIPrintf(RTTESTLVL_DEBUG, "\tMore data (Offset: %zu)\n", stream.GetOffset()); … … 249 355 RTStrmWriteEx(g_pStdOut, &g_aTestBlocks[iTest].pbData[off], cbToWrite - 1, NULL); 250 356 } 357 358 if (RTTestIErrorCount()) 359 break; 251 360 } 252 361 } 253 362 254 RTTestSub(hTest, "Blocks"); 363 RTTestSub(hTest, "Streams"); 364 255 365 for (unsigned iTest = 0; iTest < RT_ELEMENTS(g_aTestStream); iTest++) 256 366 { 257 RTTestIPrintf(RTTESTLVL_DEBUG, "=> Block test #%u\n", iTest); 367 RTTestIPrintf(RTTESTLVL_DEBUG, "=> Stream test #%u\n%.*RhXd\n", 368 iTest, g_aTestStream[iTest].cbData, g_aTestStream[iTest].pbData); 258 369 259 370 GuestToolboxStream stream; … … 261 372 if (RT_SUCCESS(iResult)) 262 373 { 263 uint32_t cBlocks = 0;264 uint8_t uSafeCouunter= 0;374 uint32_t cBlocksComplete = 0; 375 uint8_t cSafety = 0; 265 376 do 266 377 { 267 378 GuestToolboxStreamBlock curBlock; 268 379 iResult = stream.ParseBlock(curBlock); 269 RTTestIPrintf(RTTESTLVL_DEBUG, "Block #%u: Returned with %Rrc", iTest, iResult); 270 if (RT_SUCCESS(iResult)) 271 { 272 /* Only count block which have at least one pair. */ 273 if (curBlock.GetCount()) 274 cBlocks++; 275 } 276 if (uSafeCouunter++ > 32) 380 RTTestIPrintf(RTTESTLVL_DEBUG, "Stream #%u: Returned with %Rrc\n", iTest, iResult); 381 if (cSafety++ > 8) 277 382 break; 278 } while (RT_SUCCESS(iResult)); 383 if (curBlock.IsComplete()) 384 cBlocksComplete++; 385 } while (iResult != VINF_EOF); 279 386 280 387 if (iResult != g_aTestStream[iTest].iResult) 281 RTTestFailed(hTest, " Block #%uReturned %Rrc, expected %Rrc", iTest, iResult, g_aTestStream[iTest].iResult);282 else if (cBlocks != g_aTestStream[iTest].cBlocks)283 RTTestFailed(hTest, " Block #%uReturned %u blocks, expected %u", iTest, cBlocks, g_aTestStream[iTest].cBlocks);388 RTTestFailed(hTest, "Stream #%u: Returned %Rrc, expected %Rrc\n", iTest, iResult, g_aTestStream[iTest].iResult); 389 else if (cBlocksComplete != g_aTestStream[iTest].cBlocks) 390 RTTestFailed(hTest, "Stream #%u: Returned %u blocks, expected %u\n", iTest, cBlocksComplete, g_aTestStream[iTest].cBlocks); 284 391 } 285 392 else 286 RTTestFailed(hTest, "Block #%u: Adding data failed with %Rrc", iTest, iResult); 393 RTTestFailed(hTest, "Stream #%u: Adding data failed with %Rrc\n", iTest, iResult); 394 395 if (RTTestIErrorCount()) 396 break; 287 397 } 398 399 RTTestRestoreAssertions(hTest); 288 400 289 401 RTTestIPrintf(RTTESTLVL_DEBUG, "Shutting down COM...\n");
Note:
See TracChangeset
for help on using the changeset viewer.