1454 lines
35 KiB
C
1454 lines
35 KiB
C
/*++
|
|
|
|
Copyright (c) 1995-2000 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
devres2.c
|
|
|
|
Abstract:
|
|
|
|
Utility routines for resource matching
|
|
|
|
Author:
|
|
|
|
Jamie Hunter (jamiehun) 9-July-1998
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#pragma hdrstop
|
|
|
|
PRDE_LIST
|
|
pRDEList_Alloc()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Allocates a list-entry node
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
PRDE_LIST entry
|
|
|
|
--*/
|
|
{
|
|
PRDE_LIST Node;
|
|
|
|
Node = (PRDE_LIST)MyMalloc(sizeof(RDE_LIST));
|
|
if (Node == NULL) {
|
|
return NULL;
|
|
}
|
|
Node->Prev = Node;
|
|
Node->Next = Node;
|
|
Node->Entry = NULL;
|
|
return Node;
|
|
}
|
|
|
|
VOID
|
|
pRDEList_AddHead(
|
|
IN OUT PRDE_LIST pList,
|
|
IN PRDE_LIST Node
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Adds a node to head of list
|
|
|
|
Arguments:
|
|
|
|
pList = pointer to list
|
|
Node = node to add
|
|
|
|
Return Value:
|
|
|
|
none
|
|
|
|
--*/
|
|
{
|
|
MYASSERT(Node);
|
|
MYASSERT(Node->Prev == Node);
|
|
MYASSERT(Node->Next == Node);
|
|
MYASSERT(pList);
|
|
|
|
Node->Prev = pList;
|
|
Node->Next = pList->Next;
|
|
Node->Next->Prev = Node;
|
|
pList->Next = Node; // Head
|
|
}
|
|
|
|
VOID
|
|
pRDEList_AddTail(
|
|
IN OUT PRDE_LIST pList,
|
|
IN PRDE_LIST Node
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Adds a node to tail of list
|
|
|
|
Arguments:
|
|
|
|
pList = pointer to list
|
|
Node = node to add
|
|
|
|
Return Value:
|
|
|
|
none
|
|
|
|
--*/
|
|
{
|
|
MYASSERT(Node);
|
|
MYASSERT(Node->Prev == Node);
|
|
MYASSERT(Node->Next == Node);
|
|
MYASSERT(pList);
|
|
|
|
Node->Next = pList;
|
|
Node->Prev = pList->Prev;
|
|
Node->Prev->Next = Node;
|
|
pList->Prev = Node; // Tail
|
|
}
|
|
|
|
VOID
|
|
pRDEList_Remove(
|
|
IN PRDE_LIST Node
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Removes a node from list that node is member of
|
|
|
|
Arguments:
|
|
|
|
Node = node to remove
|
|
|
|
Return Value:
|
|
|
|
none
|
|
|
|
--*/
|
|
{
|
|
MYASSERT(Node);
|
|
|
|
if (Node->Prev == Node && Node->Next == Node) {
|
|
//
|
|
// already removed
|
|
//
|
|
return;
|
|
}
|
|
|
|
Node->Prev->Next = Node->Next;
|
|
Node->Next->Prev = Node->Prev;
|
|
Node->Next = Node;
|
|
Node->Prev = Node;
|
|
}
|
|
|
|
PRDE_LIST
|
|
pRDEList_Find(
|
|
IN PRDE_LIST pList,
|
|
IN PRESDES_ENTRY pItem
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Looks for pItem in pList
|
|
|
|
Arguments:
|
|
|
|
pList = list to search
|
|
pItem = item to search for
|
|
|
|
Return Value:
|
|
|
|
Node entry, or NULL
|
|
|
|
--*/
|
|
{
|
|
PRDE_LIST Node;
|
|
|
|
MYASSERT(pList);
|
|
MYASSERT(pItem);
|
|
|
|
Node = pList->Next; // head
|
|
while (Node != pList) {
|
|
if (Node->Entry == pItem) {
|
|
return Node;
|
|
}
|
|
Node = Node->Next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
VOID
|
|
pRDEList_Destroy(
|
|
IN PRDE_LIST pList
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destroy pList and everything on pList
|
|
|
|
Arguments:
|
|
|
|
pList = list to destroy
|
|
|
|
Return Value:
|
|
|
|
Node entry, or NULL
|
|
|
|
--*/
|
|
{
|
|
PRDE_LIST Node,Next;
|
|
|
|
if (pList == NULL) {
|
|
return;
|
|
}
|
|
|
|
Node = pList; // head
|
|
do
|
|
{
|
|
Next = Node->Next;
|
|
MyFree(Node); // this will free pList first, and then everything else on same list
|
|
Node = Next;
|
|
}
|
|
while (Node != pList);
|
|
}
|
|
|
|
BOOL
|
|
pGetMatchingRange(
|
|
IN ULONG64 ulKnownValue,
|
|
IN ULONG64 ulKnownLen,
|
|
IN LPBYTE pData,
|
|
IN RESOURCEID ResType,
|
|
OUT PULONG pRange,
|
|
OUT PBOOL pExact,
|
|
OUT PULONG pFlags
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Finds range index for resource inside ResDes
|
|
|
|
Arguments:
|
|
|
|
ulKnownValue - base address
|
|
ulKnownLen - length of resources
|
|
pData/ResType - resource data we're comparing with
|
|
pRange - output range index
|
|
pExact - output true if there is only one range
|
|
pFlags - output flags from matching range
|
|
|
|
Return Value:
|
|
|
|
BOOL if match
|
|
|
|
--*/
|
|
{
|
|
PGENERIC_RESOURCE pGenRes = NULL;
|
|
ULONG64 ulValue = 0, ulLen = 0, ulEnd = 0;
|
|
ULONG ulFlags = 0, i;
|
|
|
|
pGenRes = (PGENERIC_RESOURCE)pData;
|
|
|
|
for (i = 0; i < pGenRes->GENERIC_Header.GENERIC_Count; i++) {
|
|
|
|
pGetRangeValues(pData, ResType, i, &ulValue, &ulLen, &ulEnd, NULL, &ulFlags);
|
|
if (ulLen != ulKnownLen) {
|
|
continue;
|
|
}
|
|
|
|
if ((ulKnownValue >= ulValue) &&
|
|
((ulKnownValue + ulLen - 1) <= ulEnd)) {
|
|
|
|
if (pRange != NULL) {
|
|
*pRange = i;
|
|
}
|
|
//
|
|
// consider exact case
|
|
//
|
|
if (pExact != NULL) {
|
|
if (pGenRes->GENERIC_Header.GENERIC_Count==1 && ulValue == ulKnownValue && (ulKnownValue + ulLen - 1) == ulEnd) {
|
|
*pExact = TRUE;
|
|
}
|
|
}
|
|
if (pFlags != NULL) {
|
|
//
|
|
// want nearest flags
|
|
//
|
|
*pFlags = ulFlags;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
if (pRange != NULL) {
|
|
*pRange = 0;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
ULONG
|
|
pTryMatch(
|
|
IN OUT PRESDES_ENTRY pKnown,
|
|
IN OUT PRDE_LIST pResList,
|
|
IN OUT PULONG pDepth
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Looks for best match of remaining requirements in
|
|
remaining available resources
|
|
returns number of matched requirements
|
|
|
|
Arguments:
|
|
|
|
pKnown - requirements list (what is remaining)
|
|
pResList - list of available resources
|
|
|
|
Return Value:
|
|
|
|
Node entry, or NULL
|
|
|
|
--*/
|
|
{
|
|
ULONG ThisBest = 0;
|
|
ULONG MyBest = 0;
|
|
PRDE_LIST pIterator = NULL;
|
|
PRESDES_ENTRY pRange;
|
|
ULONG64 ulValue, ulLen, ulEnd;
|
|
ULONG ulFlags;
|
|
BOOL Exact = FALSE;
|
|
ULONG Best = 0;
|
|
PRDE_LIST pBestRes = NULL;
|
|
BOOL BadMatch = FALSE;
|
|
BOOL Prune = FALSE;
|
|
BOOL NoRemaining = TRUE;
|
|
|
|
MYASSERT(pDepth);
|
|
*pDepth = 0;
|
|
|
|
if (pKnown == NULL) {
|
|
//
|
|
// end recursion
|
|
//
|
|
return 0;
|
|
}
|
|
pKnown->CrossLink = NULL;
|
|
|
|
//
|
|
// we're looking for a match in pResList for pKnown
|
|
// case (1) get a "Best" for if we decide not to match
|
|
// case (2) get a "Best" for exact match, and chose between (1) and (2) if one exists
|
|
// case (3) get a "Best" for each possible range match and choose best between (1) and all (3)
|
|
//
|
|
|
|
//
|
|
// consider case(1) - what the results would be if we wasn't able to fit anything in
|
|
//
|
|
//Best = pTryMatch(pKnown->Next,pResList,pDepth);
|
|
//pBestRes = NULL;
|
|
|
|
pGetHdrValues(pKnown->ResDesData, pKnown->ResDesType, &ulValue, &ulLen, &ulEnd, &ulFlags);
|
|
|
|
//
|
|
// consider case(2) and (3) together
|
|
//
|
|
for(pIterator = pResList->Next;pIterator!=pResList;pIterator = pIterator->Next) {
|
|
//
|
|
// iterate through remaining resources
|
|
//
|
|
pRange = pIterator->Entry;
|
|
if (pRange == NULL) {
|
|
//
|
|
// this has been used
|
|
//
|
|
continue;
|
|
}
|
|
if (pRange->ResDesType != pKnown->ResDesType) {
|
|
//
|
|
// not the kind of resource i'm looking for
|
|
//
|
|
continue;
|
|
}
|
|
NoRemaining = FALSE;
|
|
if(pGetMatchingRange(ulValue, ulLen,pRange->ResDesData, pRange->ResDesType,NULL,&Exact,NULL)) {
|
|
pIterator->Entry = NULL; // eliminate this range
|
|
ThisBest = pTryMatch(pKnown->Next,pResList,pDepth); // match the rest, with us using this resource-range
|
|
pIterator->Entry = pRange;
|
|
if ((ThisBest > Best) || (pBestRes == NULL)) {
|
|
//
|
|
// the current best match (or first match if pBestRes == NULL)
|
|
//
|
|
pKnown->CrossLink = pRange;
|
|
pBestRes = pIterator;
|
|
Best = ThisBest;
|
|
MyBest = 1;
|
|
BadMatch = FALSE;
|
|
} else {
|
|
//
|
|
// need to re-do best match
|
|
//
|
|
BadMatch = TRUE;
|
|
}
|
|
if (Exact || (*pDepth == ThisBest)) {
|
|
//
|
|
// prune - we're either exact, or got a perfect match
|
|
//
|
|
Prune = TRUE;
|
|
goto Final;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (NoRemaining) {
|
|
//
|
|
// I have no resources remaining I can use - consider this as good as a match
|
|
// but we need to continue up the tree
|
|
//
|
|
Best = pTryMatch(pKnown->Next,pResList,pDepth); // match the rest, with us using this resource-range
|
|
MyBest = TRUE;
|
|
BadMatch = FALSE;
|
|
goto Final;
|
|
}
|
|
|
|
//
|
|
// if we get here we've:
|
|
// (1) found a flexable match, but wasn't able to match everyone above us, or
|
|
// (2) not found any match
|
|
// note that if last best was n with us matching and this best is n+1 without
|
|
// then we don't lose the best
|
|
// consider if overall we'd do any better if we gave up our resources to someone else
|
|
//
|
|
if((pBestRes == NULL) || ((Best+MyBest) < *pDepth)) {
|
|
//
|
|
// if we had a match, only worth checking if we could increase best by more than MyBest
|
|
// note that *pDepth is only valid if pBestRes != NULL
|
|
//
|
|
ThisBest = pTryMatch(pKnown->Next,pResList,pDepth);
|
|
if ((ThisBest > (Best+MyBest)) || (pBestRes == NULL)) {
|
|
//
|
|
// the current best match
|
|
//
|
|
pKnown->CrossLink = NULL;
|
|
pBestRes = NULL;
|
|
Best = ThisBest;
|
|
MyBest = 0;
|
|
BadMatch = FALSE;
|
|
} else {
|
|
//
|
|
// need to re-do best match
|
|
//
|
|
BadMatch = TRUE;
|
|
}
|
|
}
|
|
|
|
Final:
|
|
|
|
if (BadMatch) {
|
|
//
|
|
// We had a bad-match since our last good match
|
|
//
|
|
if (pBestRes) {
|
|
pRange = pBestRes->Entry; // the range we determined was our best bet
|
|
pBestRes->Entry = NULL; // eliminate this range
|
|
Best = pTryMatch(pKnown->Next,pResList,pDepth); // match the rest, with us using this resource-range
|
|
pBestRes->Entry = pRange;
|
|
pKnown->CrossLink = pRange;
|
|
MyBest = 1;
|
|
} else {
|
|
Best = pTryMatch(pKnown->Next,pResList,pDepth); // match the rest, with us not using this resource range
|
|
pKnown->CrossLink = NULL;
|
|
MyBest = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// if we found a match, we've saved it in pKnown->CrossLink
|
|
// return Best+0 if it's better we don't fit our resource in, Best+1 otherwise
|
|
// returns *pDepth = Best+1 if everyone (me up) fit their resources in
|
|
//
|
|
|
|
(*pDepth)++; // increment to include me
|
|
return Best+MyBest; // MyBest = 1 if I managed to match my resource (or no resources left), Best = everyone to the right of me
|
|
}
|
|
|
|
ULONG
|
|
pMergeResDesDataLists(
|
|
IN OUT PRESDES_ENTRY pKnown,
|
|
IN OUT PRESDES_ENTRY pTest,
|
|
OUT PULONG pMatchCount
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Map entries in pKnown into pTest
|
|
as best as possible
|
|
|
|
Arguments:
|
|
|
|
pKnown - list of known values
|
|
pTest - list of ranges
|
|
pMatchCount - set to be number of resources matched
|
|
|
|
Return Value:
|
|
|
|
NO_LC_MATCH if no correlation (not a single known matches)
|
|
LC_MATCH_SUPERSET if at least one known matches, but some known's don't
|
|
LC_MATCH_SUBSET if all known matches, but there are some range-items unmatched
|
|
LC_MATCH if all known matches and all range-items match
|
|
ORDERED_LC_MATCH if match, and match is in order
|
|
|
|
pKnown->CrossLink entries point to matching entries in pTest
|
|
pTest->CrossLink entries point to matching entries in pKnown
|
|
|
|
--*/
|
|
{
|
|
PRDE_LIST pResList = NULL;
|
|
PRDE_LIST Node;
|
|
PRESDES_ENTRY pKnownEntry;
|
|
PRESDES_ENTRY pTestEntry;
|
|
ULONG Success = NO_LC_MATCH;
|
|
ULONG Depth = 0;
|
|
BOOL SomeKnownMatched = FALSE;
|
|
BOOL SomeKnownUnMatched = FALSE;
|
|
BOOL SomeTestMatched = FALSE;
|
|
BOOL SomeTestUnMatched = FALSE;
|
|
BOOL Ordered = TRUE;
|
|
ULONG MatchCount = 0;
|
|
|
|
if (pKnown == NULL) {
|
|
goto Final;
|
|
}
|
|
|
|
//
|
|
// reset
|
|
//
|
|
for(pKnownEntry = pKnown; pKnownEntry != NULL ;pKnownEntry = pKnownEntry->Next) {
|
|
pKnownEntry->CrossLink = NULL;
|
|
}
|
|
|
|
for(pTestEntry = pTest; pTestEntry != NULL ;pTestEntry = pTestEntry->Next) {
|
|
pTestEntry->CrossLink = NULL;
|
|
}
|
|
|
|
pResList = pRDEList_Alloc();
|
|
|
|
if (pResList == NULL) {
|
|
goto Final;
|
|
}
|
|
|
|
//
|
|
// make all resources available
|
|
// this gives us a work list without destroying original list
|
|
//
|
|
for(pTestEntry = pTest; pTestEntry != NULL ;pTestEntry = pTestEntry->Next) {
|
|
Node = pRDEList_Alloc();
|
|
|
|
if (Node == NULL) {
|
|
goto Final;
|
|
}
|
|
Node->Entry = pTestEntry;
|
|
pRDEList_AddTail(pResList,Node);
|
|
}
|
|
|
|
MatchCount = pTryMatch(pKnown,pResList,&Depth);
|
|
|
|
if (MatchCount ==0) {
|
|
//
|
|
// no match
|
|
//
|
|
goto Final;
|
|
}
|
|
|
|
//
|
|
// pKnown now has it's Cross-Link's set to determine success of this match
|
|
//
|
|
// consider NO_LC_MATCH, LC_MATCH_SUPERSET and ORDERED_LC_MATCH cases
|
|
//
|
|
|
|
pKnownEntry = pKnown;
|
|
pTestEntry = pTest;
|
|
|
|
while (pKnownEntry) {
|
|
if (pKnownEntry->CrossLink == NULL) {
|
|
SomeKnownUnMatched = TRUE;
|
|
} else {
|
|
SomeKnownMatched = TRUE; // we have at least one matched
|
|
pKnownEntry->CrossLink->CrossLink = pKnownEntry; // cross-link test entries
|
|
if (pKnownEntry->CrossLink != pTestEntry) {
|
|
Ordered = FALSE; // ordered compare lost
|
|
} else {
|
|
pTestEntry = pTestEntry->Next; // goto next test for ordered
|
|
}
|
|
}
|
|
pKnownEntry = pKnownEntry->Next;
|
|
}
|
|
if (Ordered && pTestEntry != NULL) {
|
|
Ordered = FALSE;
|
|
}
|
|
|
|
if (SomeKnownUnMatched) {
|
|
if (SomeKnownMatched) {
|
|
Success = LC_MATCH_SUPERSET;
|
|
}
|
|
goto Final;
|
|
}
|
|
|
|
if (Ordered) {
|
|
Success = ORDERED_LC_MATCH;
|
|
goto Final;
|
|
}
|
|
|
|
//
|
|
// consider between LC_MATCH_SUBSET and LC_MATCH
|
|
//
|
|
pTestEntry = pTest;
|
|
|
|
while (pTestEntry) {
|
|
if (pTestEntry->CrossLink == NULL) {
|
|
//
|
|
// the first NULL CrossLink entry makes Known a Subset of Test
|
|
Success = LC_MATCH_SUBSET;
|
|
goto Final;
|
|
}
|
|
pTestEntry = pTestEntry->Next;
|
|
}
|
|
//
|
|
// if we get here, there is an exact match
|
|
//
|
|
Success = LC_MATCH;
|
|
|
|
Final:
|
|
pRDEList_Destroy(pResList);
|
|
|
|
if (pMatchCount != NULL) {
|
|
*pMatchCount = MatchCount;
|
|
}
|
|
|
|
return Success;
|
|
}
|
|
|
|
ULONG
|
|
pCompareLogConf(
|
|
IN LOG_CONF KnownLogConf,
|
|
IN LOG_CONF TestLogConf,
|
|
IN HMACHINE hMachine,
|
|
OUT PULONG pMatchCount
|
|
)
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine compares two log confs and returns info about how well
|
|
they match.
|
|
This simply uses the pMergeResDesDataLists function to get match status
|
|
|
|
Arguments:
|
|
|
|
KnownLogConf = First log conf to compare (fixed values)
|
|
TestConfType = Second log conf to compare (range values)
|
|
hMachine = Machine to compare on
|
|
pMatchCount = number of resources matched
|
|
|
|
Return Value:
|
|
|
|
As pMergeResDesDataLists
|
|
|
|
--*/
|
|
|
|
{
|
|
PRESDES_ENTRY pKnownResList = NULL, pTestResList = NULL;
|
|
ULONG Status;
|
|
|
|
//
|
|
// Retrieve the resources for each log conf
|
|
//
|
|
|
|
if (!pGetResDesDataList(KnownLogConf, &pKnownResList, TRUE,hMachine)) {
|
|
if (pMatchCount) {
|
|
*pMatchCount = 0;
|
|
}
|
|
return NO_LC_MATCH;
|
|
}
|
|
|
|
if (!pGetResDesDataList(TestLogConf, &pTestResList, TRUE,hMachine)) {
|
|
pDeleteResDesDataList(pKnownResList);
|
|
if (pMatchCount) {
|
|
*pMatchCount = 0;
|
|
}
|
|
return NO_LC_MATCH;
|
|
}
|
|
|
|
Status = pMergeResDesDataLists(pKnownResList,pTestResList,pMatchCount);
|
|
|
|
pDeleteResDesDataList(pKnownResList);
|
|
pDeleteResDesDataList(pTestResList);
|
|
|
|
return Status;
|
|
}
|
|
|
|
BOOL
|
|
pFindMatchingAllocConfig(
|
|
IN LPDMPROP_DATA lpdmpd
|
|
)
|
|
{
|
|
HWND hDlg = lpdmpd->hDlg;
|
|
ULONG ulBasicLC = 0;
|
|
ULONG ulBasicCount = 0;
|
|
BOOL bFoundCorrectLC = FALSE;
|
|
LOG_CONF LogConf;
|
|
ULONG lastMatchStatus = NO_LC_MATCH, bestMatchStatus = NO_LC_MATCH;
|
|
UINT lastMatchCount;
|
|
UINT bestMatchCount = 0;
|
|
HMACHINE hMachine;
|
|
|
|
hMachine = pGetMachine(lpdmpd);
|
|
|
|
lpdmpd->MatchingLC = (LOG_CONF)0;
|
|
lpdmpd->MatchingLCType = BASIC_LOG_CONF;
|
|
|
|
//
|
|
// Load the values associated with the allocated config in the list box,
|
|
// but associate each with the resource requirements descriptor that it
|
|
// originated from. To do this, we have to match the allocated config
|
|
// with the basic/filtered config it is based on.
|
|
//
|
|
// NOTE: if we got here, then we know that an known config of some kind
|
|
// exists (passed in as param) and that at least one basic/filtered config
|
|
// exists. Further more, we know that the combobox has already been
|
|
// filled in with a list of any basic/filtered configs and the lc handle
|
|
// associated with them.
|
|
//
|
|
|
|
ulBasicCount = (ULONG)SendDlgItemMessage(hDlg,IDC_DEVRES_LOGCONFIGLIST,CB_GETCOUNT,
|
|
(WPARAM)0,(LPARAM)0);
|
|
if (ulBasicCount == (ULONG)LB_ERR) {
|
|
return FALSE;
|
|
}
|
|
|
|
for (ulBasicLC = 0 ; ulBasicLC < ulBasicCount; ulBasicLC++) {
|
|
//
|
|
// Retrieve the log conf handle
|
|
//
|
|
|
|
LogConf = (LOG_CONF)SendDlgItemMessage(hDlg, IDC_DEVRES_LOGCONFIGLIST,
|
|
CB_GETITEMDATA, ulBasicLC, 0L);
|
|
|
|
if (LogConf != 0) {
|
|
//
|
|
// Determine how good a match this requirements list is.
|
|
//
|
|
|
|
lastMatchStatus = pCompareLogConf(lpdmpd->CurrentLC, LogConf,hMachine,&lastMatchCount);
|
|
if ((lastMatchStatus > bestMatchStatus)
|
|
|| ((bestMatchStatus == lastMatchStatus) && lastMatchCount > bestMatchCount)) {
|
|
bestMatchCount = lastMatchCount;
|
|
bestMatchStatus =lastMatchStatus;
|
|
lpdmpd->MatchingLC = LogConf;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMatchStatus == NO_LC_MATCH || bestMatchStatus == LC_MATCH_SUBSET) {
|
|
//
|
|
// this doesn't follow any valid config
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
lpdmpd->dwFlags &= ~(DMPROP_FLAG_PARTIAL_MATCH|DMPROP_FLAG_MATCH_OUT_OF_ORDER);
|
|
if (bestMatchStatus != ORDERED_LC_MATCH) {
|
|
//
|
|
// If match status isn't ORDERED_LC_MATCH, then ordering of the resource descriptors
|
|
// didn't match up. Set a flag to indicate this, so that later on we'll know to handle
|
|
// this specially.
|
|
//
|
|
lpdmpd->dwFlags |= DMPROP_FLAG_MATCH_OUT_OF_ORDER;
|
|
} else if (bestMatchStatus < LC_MATCH) {
|
|
//
|
|
// match is partial
|
|
//
|
|
lpdmpd->dwFlags |= DMPROP_FLAG_PARTIAL_MATCH;
|
|
}
|
|
return TRUE;
|
|
|
|
} // LoadMatchingAllocConfig
|
|
|
|
BOOL
|
|
pGetMatchingResDes(
|
|
IN ULONG64 ulKnownValue,
|
|
IN ULONG64 ulKnownLen,
|
|
IN ULONG64 ulKnownEnd,
|
|
IN RESOURCEID ResType,
|
|
IN LOG_CONF MatchingLogConf,
|
|
OUT PRES_DES pMatchingResDes,
|
|
IN HMACHINE hMachine
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This returns a res des that matches the specified values.
|
|
used by pSaveCustomResSettings
|
|
|
|
Arguments:
|
|
|
|
ulKnownValue Starting resource value to match against
|
|
|
|
ulKnownLen Length of resource to match against
|
|
|
|
ResType Type of resource to match against
|
|
|
|
MatchnigLogConf Log conf to retreive potential matching res des from
|
|
|
|
pMatchingResDes Supplies a pointer that on return contains a matching
|
|
res des if any.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
|
|
{
|
|
CONFIGRET Status = CR_SUCCESS;
|
|
RESOURCEID Res;
|
|
RES_DES ResDes, ResDesTemp;
|
|
ULONG64 ulValue = 0, ulLen = 0, ulEnd = 0;
|
|
ULONG ulSize, ulFlags = 0, i;
|
|
PGENERIC_RESOURCE pGenRes;
|
|
BOOL bMatch = FALSE;
|
|
LPBYTE pData = NULL;
|
|
|
|
|
|
//
|
|
// The MatchingLogConf is a requirements list. Loop through each res des
|
|
// in the matching log conf until we find a res des that matches the
|
|
// known res des values.
|
|
//
|
|
Status = CM_Get_Next_Res_Des_Ex(&ResDes, MatchingLogConf, ResType, &Res, 0,hMachine);
|
|
|
|
while (Status == CR_SUCCESS) {
|
|
//
|
|
// Get res des data
|
|
//
|
|
if (CM_Get_Res_Des_Data_Size_Ex(&ulSize, ResDes, CM_RESDES_WIDTH_64,hMachine) != CR_SUCCESS) {
|
|
CM_Free_Res_Des_Handle(ResDes);
|
|
break;
|
|
}
|
|
|
|
pData = MyMalloc(ulSize);
|
|
if (pData == NULL) {
|
|
CM_Free_Res_Des_Handle(ResDes);
|
|
break;
|
|
}
|
|
|
|
if (CM_Get_Res_Des_Data_Ex(ResDes, pData, ulSize, DEVRES_WIDTH_FLAGS,hMachine) != CR_SUCCESS) {
|
|
CM_Free_Res_Des_Handle(ResDes);
|
|
MyFree(pData);
|
|
break;
|
|
}
|
|
|
|
if(pGetMatchingRange(ulKnownValue,ulKnownLen,pData,ResType,NULL,NULL,NULL)) {
|
|
*pMatchingResDes = ResDes;
|
|
bMatch = TRUE;
|
|
MyFree(pData);
|
|
goto MatchFound;
|
|
}
|
|
|
|
//
|
|
// Get next res des in log conf
|
|
//
|
|
ResDesTemp = ResDes;
|
|
Status = CM_Get_Next_Res_Des_Ex(&ResDes, ResDesTemp,
|
|
ResType, &Res, 0,hMachine);
|
|
|
|
CM_Free_Res_Des_Handle(ResDesTemp);
|
|
MyFree(pData);
|
|
}
|
|
|
|
MatchFound:
|
|
|
|
return bMatch;
|
|
|
|
} // GetMatchingResDes
|
|
|
|
|
|
|
|
//
|
|
// NTRAID#166214-2000/08/19-JamieHun Conflict Supression Hack
|
|
// this stuff needs to be fixed proper
|
|
//
|
|
|
|
PTSTR
|
|
pGetRegString(
|
|
IN HKEY hKey,
|
|
IN PCTSTR regval
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Obtain and return a registry string allocated by MyMalloc
|
|
return NULL if we couldn't retrieve string
|
|
|
|
Arguments:
|
|
|
|
hKey - key to retrieve string from
|
|
regval - value to retrieve
|
|
|
|
Return Value:
|
|
|
|
copy of registry string, may be free'd with MyFree
|
|
|
|
--*/
|
|
{
|
|
DWORD dwSize;
|
|
DWORD dwType;
|
|
PTSTR pSz;
|
|
LONG res;
|
|
|
|
dwType = 0;
|
|
dwSize = 0;
|
|
res = RegQueryValueEx(hKey,regval,NULL,&dwType,(PBYTE)NULL,&dwSize);
|
|
if (res != ERROR_SUCCESS) {
|
|
return NULL;
|
|
}
|
|
if (dwType != REG_SZ) {
|
|
return NULL;
|
|
}
|
|
pSz = MyMalloc(dwSize);
|
|
if (pSz == NULL) {
|
|
return NULL;
|
|
}
|
|
res = RegQueryValueEx(hKey,regval,NULL,&dwType,(PBYTE)pSz,&dwSize);
|
|
if (res != ERROR_SUCCESS) {
|
|
MyFree(pSz);
|
|
return NULL;
|
|
}
|
|
return pSz;
|
|
}
|
|
|
|
VOID
|
|
pFillCETags(
|
|
IN PCONFLICT_EXCEPTIONS pExceptions,
|
|
IN PCE_TAGS pTags,
|
|
PTSTR pSz
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
parse a list of tags into CE_TAGS structure
|
|
adding the strings into the string table en-route
|
|
note that this structure will be flexable and allow ',' or ';' seperator
|
|
however when used in Exceptions string, we've already eliminated any ';'
|
|
|
|
format is:
|
|
<tag>,<tag>,<tag> or <tag>;<tag>;<tag>
|
|
|
|
Arguments:
|
|
|
|
pExceptions - context information
|
|
pTags - tag structure to fill in
|
|
|
|
Return Value:
|
|
|
|
none
|
|
|
|
--*/
|
|
{
|
|
static CE_TAGS DummyEntry = { -1 }; // if we write a new string, negative size count means this isn't a devnode entry
|
|
|
|
MYASSERT(pTags->nTags == 0);
|
|
|
|
while(pSz[0] && pTags->nTags < MAX_CE_TAGS) {
|
|
if(pSz[0]==TEXT(',')||pSz[0]==TEXT(';')||pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
} else {
|
|
PTSTR pOldSz = pSz;
|
|
PTSTR pLastSpace = NULL;
|
|
LONG id;
|
|
while (pSz[0] && pSz[0]!=TEXT(';')&& pSz[0]!=TEXT(',')) {
|
|
if (pSz[0]<=TEXT(' ')) {
|
|
if (pLastSpace==NULL) {
|
|
pLastSpace = pSz;
|
|
}
|
|
} else {
|
|
pLastSpace = NULL;
|
|
}
|
|
pSz++;
|
|
}
|
|
//
|
|
// pSz points to '\0', ';' or ','
|
|
// pLastSpace points to any trailing WS
|
|
// pOldSz points to start of string
|
|
//
|
|
if(pLastSpace==NULL) {
|
|
pLastSpace = pSz;
|
|
}
|
|
if (pSz[0]) {
|
|
pSz++;
|
|
}
|
|
pLastSpace[0]=TEXT('\0');
|
|
//
|
|
// pSz points to next string, pOldSz points to this string
|
|
// add string to string table, place in list of tags
|
|
//
|
|
id = pSetupStringTableAddStringEx(pExceptions->ceTagMap,pOldSz,STRTAB_CASE_INSENSITIVE|STRTAB_BUFFER_WRITEABLE,&DummyEntry,sizeof(DummyEntry));
|
|
if (id>=0) {
|
|
pTags->Tag[pTags->nTags++] = id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PCE_ENTRY
|
|
pScanConflictExceptionEntry(
|
|
IN PCONFLICT_EXCEPTIONS pExceptions,
|
|
PTSTR pSz
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
obtains conflict exception info from string
|
|
|
|
format is:
|
|
(1) <tags> - always ignore tag for any type of conflict
|
|
(2) <rt>:<tags> - ignore tag for <rt> resource type
|
|
(3) <rt>@x:<tags> - IRQ/DMA - specfic
|
|
(4) <rt>@x-y:<tags> - IO/MEM - range
|
|
|
|
<tags> are a comma-sep list of tags <tag>,<tag>,<tag>
|
|
|
|
Arguments:
|
|
|
|
pExceptions - context information
|
|
pSz - string to parse
|
|
|
|
Return Value:
|
|
|
|
CE_ENTRY structure if this is a valid descriptor
|
|
|
|
--*/
|
|
{
|
|
PTSTR brk;
|
|
PCE_ENTRY pEntry;
|
|
TCHAR rt[5];
|
|
int c;
|
|
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
if (!pSz[0]) {
|
|
return NULL;
|
|
}
|
|
|
|
pEntry = MyMalloc(sizeof(CE_ENTRY));
|
|
|
|
if (pEntry == NULL) {
|
|
return NULL;
|
|
}
|
|
ZeroMemory(pEntry,sizeof(CE_ENTRY));
|
|
|
|
brk = _tcschr(pSz,TEXT(':'));
|
|
|
|
if(!brk) {
|
|
//
|
|
// treat as tags only
|
|
//
|
|
pEntry->resType = ResType_None;
|
|
} else {
|
|
//
|
|
// determine resource type
|
|
//
|
|
for(c=0;_istalpha(pSz[0]) && c<(sizeof(rt)/sizeof(TCHAR)-1);c++,pSz++) {
|
|
rt[c] = (TCHAR)_totupper(pSz[0]);
|
|
}
|
|
rt[c] = 0;
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
if (pSz[0]!=TEXT(':') && pSz[0]!=TEXT('@')) {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
} else if (lstrcmp(rt,CE_RES_IO)==0) {
|
|
pEntry->resType = ResType_IO;
|
|
} else if (lstrcmp(rt,CE_RES_MEM)==0) {
|
|
pEntry->resType = ResType_Mem;
|
|
} else if (lstrcmp(rt,CE_RES_IRQ)==0) {
|
|
pEntry->resType = ResType_IRQ;
|
|
} else if (lstrcmp(rt,CE_RES_DMA)==0) {
|
|
pEntry->resType = ResType_DMA;
|
|
} else {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
}
|
|
if (pSz[0]!=TEXT('@')) {
|
|
//
|
|
// no range follows
|
|
//
|
|
pEntry->resStart = (ULONG64)0;
|
|
pEntry->resEnd = (ULONG64)(-1);
|
|
} else {
|
|
//
|
|
// @x[-y]:
|
|
//
|
|
ULONG x;
|
|
ULONG y;
|
|
PTSTR i;
|
|
|
|
pSz++; // past @
|
|
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
i = pSz;
|
|
x = _tcstoul(pSz,&i,0);
|
|
if (i==pSz) {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
}
|
|
pSz = i;
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
if (pSz[0]==TEXT('-')) {
|
|
//
|
|
// -y
|
|
//
|
|
pSz++;
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
i = pSz;
|
|
y = _tcstoul(pSz,&i,0);
|
|
if (i==pSz || y<x) {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
}
|
|
pSz = i;
|
|
while (pSz[0] && pSz[0]<=TEXT(' ')) {
|
|
pSz++;
|
|
}
|
|
} else {
|
|
y = x;
|
|
}
|
|
pEntry->resStart = x;
|
|
pEntry->resEnd = y;
|
|
}
|
|
if (pSz[0] != TEXT(':')) {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
}
|
|
pSz ++; // skip past colon
|
|
}
|
|
//
|
|
// at this point, expect a list of tags
|
|
// each tag terminated by a comma
|
|
//
|
|
pFillCETags(pExceptions,&pEntry->tags,pSz);
|
|
if (!pEntry->tags.nTags) {
|
|
MyFree(pEntry);
|
|
return NULL;
|
|
}
|
|
return pEntry;
|
|
}
|
|
|
|
PCONFLICT_EXCEPTIONS pLoadConflictExceptions(
|
|
IN LPDMPROP_DATA lpdmpd
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Load the string "ResourcePickerExceptions" if any
|
|
create a context structure for eliminating false conflicts
|
|
this is one huge hack.
|
|
|
|
Arguments:
|
|
|
|
lpdmpd - Context data
|
|
|
|
Return Value:
|
|
|
|
CONFLICT_EXCEPTIONS structure, if "active" contains a string table and a list of resource exceptions
|
|
|
|
--*/
|
|
{
|
|
PCONFLICT_EXCEPTIONS pExceptions;
|
|
PCE_ENTRY pEntry;
|
|
BOOL bStatus;
|
|
HKEY hDevRegKey;
|
|
PTSTR pSz;
|
|
PTSTR pScanSz;
|
|
PTSTR pOldSz;
|
|
PCE_TAGS pTags;
|
|
|
|
//
|
|
// we always create the structure, so we will avoid looking for registry info every time
|
|
//
|
|
pExceptions = MyMalloc(sizeof(CONFLICT_EXCEPTIONS));
|
|
if (pExceptions == NULL) {
|
|
return NULL;
|
|
}
|
|
ZeroMemory(pExceptions,sizeof(CONFLICT_EXCEPTIONS));
|
|
|
|
hDevRegKey = SetupDiOpenDevRegKey(lpdmpd->hDevInfo,lpdmpd->lpdi,DICS_FLAG_GLOBAL,0,DIREG_DRV,KEY_READ);
|
|
if (hDevRegKey == INVALID_HANDLE_VALUE) {
|
|
//
|
|
// can't find key, no conflict elimination
|
|
//
|
|
return pExceptions;
|
|
}
|
|
pSz = pGetRegString(hDevRegKey,REGSTR_VAL_RESOURCE_PICKER_EXCEPTIONS);
|
|
RegCloseKey(hDevRegKey);
|
|
if(pSz == NULL) {
|
|
//
|
|
// can't find key, no conflict elimination
|
|
//
|
|
return pExceptions;
|
|
}
|
|
|
|
//
|
|
// now parse the string creating our context to eliminate false conflicts
|
|
//
|
|
|
|
pExceptions->ceTagMap = pSetupStringTableInitializeEx(sizeof(CE_TAGS),0);
|
|
if (pExceptions->ceTagMap == NULL) {
|
|
MyFree(pSz);
|
|
return pExceptions;
|
|
}
|
|
|
|
pScanSz = pSz;
|
|
|
|
while (pScanSz[0]) {
|
|
if (pScanSz[0] == TEXT(';')) {
|
|
pScanSz ++;
|
|
} else {
|
|
pOldSz = pScanSz;
|
|
while (pScanSz[0] && pScanSz[0] != TEXT(';')) {
|
|
pScanSz++;
|
|
}
|
|
if (pScanSz[0]) {
|
|
pScanSz[0] = 0;
|
|
pScanSz++;
|
|
}
|
|
pEntry = pScanConflictExceptionEntry(pExceptions,pOldSz);
|
|
if (pEntry) {
|
|
pEntry->Next = pExceptions->exceptions;
|
|
pExceptions->exceptions = pEntry;
|
|
}
|
|
}
|
|
}
|
|
|
|
MyFree(pSz);
|
|
return pExceptions;
|
|
}
|
|
|
|
VOID pFreeConflictExceptions(
|
|
IN PCONFLICT_EXCEPTIONS pExceptions
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Releases memory used by PCONFLICT_EXCEPTIONS
|
|
|
|
Arguments:
|
|
|
|
pExceptions structure to release
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// free the list
|
|
//
|
|
while (pExceptions->exceptions) {
|
|
PCE_ENTRY pEntry = pExceptions->exceptions;
|
|
pExceptions->exceptions = pEntry->Next;
|
|
MyFree(pEntry);
|
|
}
|
|
//
|
|
// free the string table
|
|
//
|
|
if (pExceptions->ceTagMap) {
|
|
pSetupStringTableDestroy(pExceptions->ceTagMap);
|
|
}
|
|
MyFree(pExceptions);
|
|
}
|
|
|
|
BOOL pIsConflictException(
|
|
IN LPDMPROP_DATA lpdmpd,
|
|
IN PCONFLICT_EXCEPTIONS pExceptions,
|
|
IN DEVINST devConflict,
|
|
IN PCTSTR resDesc,
|
|
IN RESOURCEID resType,
|
|
IN ULONG64 resValue,
|
|
IN ULONG64 resLength
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Load the string "ResourcePickerExceptions" if any
|
|
create a context structure for eliminating false conflicts
|
|
this is one huge hack.
|
|
|
|
Arguments:
|
|
|
|
lpdmpd - Context data
|
|
pExceptions - Cache of information
|
|
devConflict - DEVINST that's shown to be conflicting with us, -1 if "unavailable" (tag = *)
|
|
resType - type of resource that we tested
|
|
resValue - value of resource that we tested
|
|
resLength - length of resource that we tested
|
|
|
|
Return Value:
|
|
|
|
TRUE if this is an exception
|
|
|
|
--*/
|
|
{
|
|
HMACHINE hMachine;
|
|
TCHAR DevNodeName[MAX_DEVNODE_ID_LEN];
|
|
CE_TAGS tags;
|
|
PCE_ENTRY pEntry;
|
|
LONG tagent;
|
|
LONG n,m;
|
|
ULONG64 resEnd = resValue+(resLength-1);
|
|
PTSTR pSz;
|
|
HKEY hKey;
|
|
|
|
//
|
|
// if we're not doing any exceptions, get out ASAP
|
|
//
|
|
if (pExceptions->exceptions == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
hMachine = pGetMachine(lpdmpd);
|
|
//
|
|
// handle "reserved" case first
|
|
//
|
|
if (devConflict != -1) {
|
|
//
|
|
// obtain device instance string
|
|
//
|
|
if(CM_Get_Device_ID_Ex(devConflict,DevNodeName,MAX_DEVNODE_ID_LEN,0,hMachine)!=CR_SUCCESS) {
|
|
devConflict = -1;
|
|
}
|
|
}
|
|
if (devConflict == -1) {
|
|
if (resDesc && resDesc[0]) {
|
|
lstrcpy(DevNodeName,resDesc);
|
|
} else {
|
|
lstrcpy(DevNodeName,CE_TAG_RESERVED);
|
|
}
|
|
} else {
|
|
}
|
|
//
|
|
// is this a brand-new devnodename ?
|
|
//
|
|
tags.nTags = 0;
|
|
tagent = pSetupStringTableLookUpStringEx(pExceptions->ceTagMap,DevNodeName,STRTAB_CASE_INSENSITIVE|STRTAB_BUFFER_WRITEABLE,&tags,sizeof(tags));
|
|
if(tagent<0 || tags.nTags<0) {
|
|
//
|
|
// this particular devnode hasn't been processed before, ouch time
|
|
//
|
|
ZeroMemory(&tags,sizeof(tags)); // default reserved case
|
|
if (devConflict != -1) {
|
|
//
|
|
// we need to get regkey for this devnode
|
|
// I could do this via setupapi, or cfgmgr
|
|
// for efficiency, I'm going latter route
|
|
//
|
|
if(CM_Open_DevNode_Key_Ex(devConflict,
|
|
KEY_READ,
|
|
0,
|
|
RegDisposition_OpenExisting,
|
|
&hKey,
|
|
CM_REGISTRY_SOFTWARE,
|
|
hMachine) == CR_SUCCESS) {
|
|
|
|
pSz = pGetRegString(hKey,REGSTR_VAL_RESOURCE_PICKER_TAGS);
|
|
RegCloseKey(hKey);
|
|
|
|
if (pSz) {
|
|
//
|
|
// now fill in tags
|
|
//
|
|
pFillCETags(pExceptions,&tags,pSz);
|
|
MyFree(pSz);
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// now write this back into the string table
|
|
// this time, non-negative nTags indicates we've processed this once
|
|
// we will re-write the extra-data
|
|
//
|
|
tagent = pSetupStringTableAddStringEx(pExceptions->ceTagMap,DevNodeName,STRTAB_CASE_INSENSITIVE|STRTAB_BUFFER_WRITEABLE|STRTAB_NEW_EXTRADATA,&tags,sizeof(tags));
|
|
}
|
|
if (tagent<0) {
|
|
//
|
|
// if this happens, we have other problems (out of memory)
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// go through our list of exceptions
|
|
// an exception that mentions tagent, or any of tags associated with tagent (in tags) is rejected
|
|
// the policy in this routine is make this an exception if we can
|
|
//
|
|
|
|
for(pEntry=pExceptions->exceptions;pEntry;pEntry=pEntry->Next) {
|
|
|
|
if (pEntry->resType != ResType_None) {
|
|
//
|
|
// we need to validate the resource
|
|
//
|
|
if (pEntry->resType != resType ||
|
|
pEntry->resStart > resValue ||
|
|
pEntry->resEnd < resEnd) {
|
|
continue;
|
|
}
|
|
}
|
|
for (n=0;n<pEntry->tags.nTags;n++) {
|
|
if (pEntry->tags.Tag[n] == tagent) {
|
|
MYTRACE((DPFLTR_ERROR_LEVEL, TEXT("Setup: Eliminated false conflict with %s type=%u, start=0x%08x, len=0x%08x\n"),DevNodeName,resType,resValue,resLength));
|
|
return TRUE; // hit (devnode itself, where devnode may also be "*")
|
|
}
|
|
for (m=0;m<tags.nTags;m++) {
|
|
if (pEntry->tags.Tag[n] == tags.Tag[m]) {
|
|
MYTRACE((DPFLTR_ERROR_LEVEL, TEXT("Setup: Eliminated false conflict with %s (via tag %s) type=%u, start=0x%08x, len=0x%08x\n"),DevNodeName,pStringTableStringFromId(pExceptions->ceTagMap,tags.Tag[m]),resType,resValue,resLength));
|
|
return TRUE; // hit on one of associated tags
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|