715 lines
17 KiB
C
715 lines
17 KiB
C
/*++
|
|
|
|
Copyright (c) 1997 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
lookup.c
|
|
|
|
Abstract:
|
|
|
|
This module contains routines for a wrapper
|
|
that integrates the trie lookup into TCPIP.
|
|
|
|
Author:
|
|
|
|
Chaitanya Kodeboyina (chaitk) 11-Dec-1997
|
|
|
|
Revision History:
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "lookup.h"
|
|
#include "info.h"
|
|
|
|
// Wrapper Constants
|
|
|
|
// MaskBitsArr[i] = First 'i' bits set to 1 in an unsigned long
|
|
const ULONG MaskBitsArr[] =
|
|
{
|
|
0x00000000, 0x80000000, 0xC0000000, 0xE0000000,
|
|
0xF0000000, 0xF8000000, 0xFC000000, 0xFE000000,
|
|
0xFF000000, 0xFF800000, 0xFFC00000, 0xFFE00000,
|
|
0xFFF00000, 0xFFF80000, 0xFFFC0000, 0xFFFE0000,
|
|
0xFFFF0000, 0xFFFF8000, 0xFFFFC000, 0xFFFFE000,
|
|
0xFFFFF000, 0xFFFFF800, 0xFFFFFC00, 0xFFFFFE00,
|
|
0xFFFFFF00, 0xFFFFFF80, 0xFFFFFFC0, 0xFFFFFFE0,
|
|
0xFFFFFFF0, 0xFFFFFFF8, 0xFFFFFFFC, 0xFFFFFFFE,
|
|
0xFFFFFFFF
|
|
};
|
|
|
|
// Wrapper Globals
|
|
|
|
// IP Route Table
|
|
Trie *RouteTable;
|
|
|
|
// Eq Cost Routes
|
|
USHORT MaxEqualCostRoutes = 0;
|
|
|
|
extern uint DefGWActive;
|
|
extern uint DefGWConfigured;
|
|
extern uint ValidateDefaultGWs(IPAddr Addr);
|
|
|
|
UINT
|
|
InsRoute(IPAddr Dest, IPMask Mask, IPAddr FirstHop, VOID * OutIF,
|
|
UINT Metric, ULONG MatchFlags, RouteTableEntry ** ppInsRTE,
|
|
RouteTableEntry ** ppOldBestRTE, RouteTableEntry ** ppNewBestRTE)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Inserts a route into the route table
|
|
|
|
We update only
|
|
1) Dest Addr,
|
|
2) Dest Mask,
|
|
3) Priority,
|
|
4) Route Metric
|
|
|
|
The rest of the RTE fields are left
|
|
untouched - to enable the caller to
|
|
read the old values (if this route
|
|
already existed in the route table)
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Dest - Destination IP Addr
|
|
Mask - Destination IP Mask
|
|
FirstHop - IP Addr of Next Hop
|
|
OutIF - Outgoing Interface
|
|
Metric - Metric for the route
|
|
MatchFlags - RTE Fields to match
|
|
OUT -
|
|
ppInsRTE - Ptr to Ptr to new/updated RTE
|
|
ppOldBestRTE - Ptr to Ptr to old best RTE
|
|
ppNewBestRTE - Ptr to Ptr to new best RTE
|
|
|
|
Return Value:
|
|
|
|
STATUS_SUCCESS or Error Code
|
|
|
|
--*/
|
|
{
|
|
Route route;
|
|
ULONG temp;
|
|
|
|
DEST(&route) = Dest;
|
|
MASK(&route) = Mask;
|
|
NHOP(&route) = FirstHop;
|
|
IF(&route) = OutIF;
|
|
|
|
temp = RtlConvertEndianLong(Mask);
|
|
LEN(&route) = 0;
|
|
while (temp != 0) {
|
|
LEN(&route)++;
|
|
temp <<= 1;
|
|
}
|
|
|
|
METRIC(&route) = Metric;
|
|
|
|
switch (InsertIntoTrie(RouteTable, &route, MatchFlags,
|
|
ppInsRTE, ppOldBestRTE, ppNewBestRTE)) {
|
|
case TRIE_SUCCESS:
|
|
return IP_SUCCESS;
|
|
case ERROR_TRIE_BAD_PARAM:
|
|
return IP_BAD_REQ;
|
|
case ERROR_TRIE_RESOURCES:
|
|
return IP_NO_RESOURCES;
|
|
}
|
|
|
|
Assert(FALSE);
|
|
return IP_GENERAL_FAILURE;
|
|
}
|
|
|
|
UINT
|
|
DelRoute(IPAddr Dest, IPMask Mask, IPAddr FirstHop, VOID * OutIF,
|
|
ULONG MatchFlags, RouteTableEntry ** ppDelRTE,
|
|
RouteTableEntry ** ppOldBestRTE, RouteTableEntry ** ppNewBestRTE)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Deletes a route from the route table
|
|
|
|
The memory for the route(allocated
|
|
on the heap) should be deallocated
|
|
upon return, after all information
|
|
required is read and processed.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Dest - Destination IP Addr
|
|
Mask - Destination IP Mask
|
|
FirstHop - IP Addr of Next Hop
|
|
OutIF - Outgoing Interface
|
|
Metric - Metric for the route
|
|
MatchFlags - RTE Fields to match
|
|
OUT -
|
|
ppDelRTE - Ptr to Ptr to the deleted RTE
|
|
ppOldBestRTE - Ptr to Ptr to old best RTE
|
|
ppNewBestRTE - Ptr to Ptr to new best RTE
|
|
|
|
Return Value:
|
|
|
|
STATUS_SUCCESS or Error Code
|
|
|
|
--*/
|
|
{
|
|
Route route;
|
|
ULONG temp;
|
|
|
|
DEST(&route) = Dest;
|
|
MASK(&route) = Mask;
|
|
NHOP(&route) = FirstHop;
|
|
IF(&route) = OutIF;
|
|
|
|
temp = RtlConvertEndianLong(Mask);
|
|
LEN(&route) = 0;
|
|
while (temp != 0) {
|
|
LEN(&route)++;
|
|
temp <<= 1;
|
|
}
|
|
|
|
switch (DeleteFromTrie(RouteTable, &route, MatchFlags,
|
|
ppDelRTE, ppOldBestRTE, ppNewBestRTE)) {
|
|
case TRIE_SUCCESS:
|
|
return IP_SUCCESS;
|
|
case ERROR_TRIE_NO_ROUTES:
|
|
return IP_BAD_ROUTE;
|
|
case ERROR_TRIE_BAD_PARAM:
|
|
return IP_BAD_REQ;
|
|
case ERROR_TRIE_RESOURCES:
|
|
return IP_NO_RESOURCES;
|
|
}
|
|
|
|
Assert(FALSE);
|
|
return IP_GENERAL_FAILURE;
|
|
}
|
|
|
|
RouteTableEntry *
|
|
FindRTE(IPAddr Dest, IPAddr Source, UINT Index, UINT MaxPri, UINT MinPri, UINT UnicastIf)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Searches for a route given a prefix,
|
|
with a mask len between the given
|
|
minimum and maximum values.
|
|
|
|
The route returned is a Semi-Read-
|
|
Only-Version. The following fields
|
|
should be changed only by calling
|
|
the InsRoute function -
|
|
1) Next,
|
|
2) Dest,
|
|
3) Mask,
|
|
4) Priority, &
|
|
5) Route Metric.
|
|
|
|
Remaining fields can be changed by
|
|
directly modifying returned route.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Dest - Destination IP Addr
|
|
Source - Source to match IF
|
|
Index - *Value is ignored*
|
|
MaxPri - Max mask len of RTE
|
|
MinPri - Min mask len of RTE
|
|
|
|
Return Value:
|
|
|
|
Matching RTE or NULL if not match
|
|
|
|
--*/
|
|
{
|
|
RouteTableEntry *pBestRoute;
|
|
RouteTableEntry *pCurrRoute;
|
|
ULONG addr;
|
|
ULONG mask;
|
|
INT lookupPri;
|
|
|
|
// Start looking for most specific match
|
|
lookupPri = MaxPri;
|
|
|
|
do {
|
|
// Use lookupPri to mask xs bits
|
|
addr = RtlConvertEndianLong(Dest);
|
|
addr = ShowMostSigNBits(addr, lookupPri);
|
|
Dest = RtlConvertEndianLong(addr);
|
|
|
|
addr = ShowMostSigNBits(~0, lookupPri);
|
|
mask = RtlConvertEndianLong(addr);
|
|
|
|
// Try to match destination
|
|
SearchRouteInTrie(RouteTable,
|
|
Dest,
|
|
mask,
|
|
0, NULL,
|
|
MATCH_NONE,
|
|
&pBestRoute);
|
|
|
|
if ((NULL_ROUTE(pBestRoute)) || (LEN(pBestRoute) < MinPri)) {
|
|
return NULL;
|
|
}
|
|
// Just in case we need to loop
|
|
lookupPri = LEN(pBestRoute) - 1;
|
|
|
|
// Search for a valid route
|
|
while (pBestRoute) {
|
|
if ((FLAGS(pBestRoute) & RTE_VALID) && (!(FLAGS(pBestRoute) & RTE_DEADGW)))
|
|
break;
|
|
|
|
pBestRoute = NEXT(pBestRoute);
|
|
}
|
|
|
|
// Do we match source too ?
|
|
if (!IP_ADDR_EQUAL(Source, NULL_IP_ADDR) || UnicastIf) {
|
|
// Dest match - Match source
|
|
pCurrRoute = pBestRoute;
|
|
while (pCurrRoute) {
|
|
if (!UnicastIf) {
|
|
if (METRIC(pCurrRoute) > METRIC(pBestRoute)) {
|
|
// No Source match
|
|
break;
|
|
}
|
|
}
|
|
// Get next valid route
|
|
if (((FLAGS(pCurrRoute) & RTE_VALID) && (!(FLAGS(pCurrRoute) & RTE_DEADGW))) &&
|
|
((!IP_ADDR_EQUAL(Source, NULL_IP_ADDR) &&
|
|
AddrOnIF(IF(pCurrRoute), Source)) ||
|
|
(UnicastIf &&
|
|
IF(pCurrRoute)->if_index == UnicastIf))) {
|
|
// Source match too
|
|
pBestRoute = pCurrRoute;
|
|
break;
|
|
}
|
|
pCurrRoute = NEXT(pCurrRoute);
|
|
}
|
|
|
|
if (UnicastIf && (pCurrRoute == NULL)) {
|
|
pBestRoute = NULL;
|
|
}
|
|
}
|
|
}
|
|
while ((NULL_ROUTE(pBestRoute)) && (lookupPri >= (INT) MinPri));
|
|
|
|
return pBestRoute;
|
|
}
|
|
|
|
RouteTableEntry *
|
|
LookupRTE(IPAddr Dest, IPAddr Source, UINT MaxPri, UINT UnicastIf)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Searches for a best route for IP addr.
|
|
|
|
The route returned is a Semi-Read-
|
|
Only-Version. The following fields
|
|
should be changed only by calling
|
|
the InsRoute function -
|
|
1) Next,
|
|
2) Dest,
|
|
3) Mask,
|
|
4) Priority, &
|
|
5) Route Metric.
|
|
|
|
Remaining fields can be changed by
|
|
directly modifying returned route.
|
|
|
|
Comments:
|
|
*LookupRTE* assumes that VALID flag
|
|
can be set on/off only for default
|
|
routes. Because in case we find a
|
|
chain with all invalid routes we do
|
|
not enough information to go up in
|
|
the F-trie for less specific routes
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Dest - Destination IP Addr
|
|
Source - Source to match IF
|
|
MaxPri - *Value is ignored*
|
|
|
|
Return Value:
|
|
|
|
Matching RTE or NULL if not match
|
|
|
|
--*/
|
|
{
|
|
DestinationEntry *pBestDest;
|
|
RouteTableEntry *pBestRoute;
|
|
RouteTableEntry *pCurrRoute;
|
|
UINT routeIndex;
|
|
|
|
// Try to match destination
|
|
pBestDest = SearchAddrInTrie(RouteTable, Dest);
|
|
|
|
// No prefix match - quit
|
|
if (pBestDest == NULL) {
|
|
return NULL;
|
|
}
|
|
// Search for a valid route
|
|
pBestRoute = pBestDest->firstRoute;
|
|
|
|
while (pBestRoute) {
|
|
if ((FLAGS(pBestRoute) & RTE_VALID) && (!(FLAGS(pBestRoute) & RTE_DEADGW)))
|
|
break;
|
|
|
|
pBestRoute = NEXT(pBestRoute);
|
|
}
|
|
|
|
// Do we match source too ?
|
|
if (!IP_ADDR_EQUAL(Source, NULL_IP_ADDR) || UnicastIf) {
|
|
// Dest match - Match source
|
|
pCurrRoute = pBestRoute;
|
|
while (pCurrRoute) {
|
|
// Are we doing a weak host lookup ?
|
|
if (!UnicastIf) {
|
|
if (METRIC(pCurrRoute) > METRIC(pBestRoute)) {
|
|
// No Source match
|
|
break;
|
|
}
|
|
}
|
|
// Get next valid route
|
|
if (((FLAGS(pCurrRoute) & RTE_VALID) && (!(FLAGS(pCurrRoute) & RTE_DEADGW))) &&
|
|
((!IP_ADDR_EQUAL(Source, NULL_IP_ADDR) &&
|
|
AddrOnIF(IF(pCurrRoute), Source)) ||
|
|
(UnicastIf &&
|
|
IF(pCurrRoute)->if_index == UnicastIf))) {
|
|
// Source match too
|
|
pBestRoute = pCurrRoute;
|
|
break;
|
|
}
|
|
pCurrRoute = NEXT(pCurrRoute);
|
|
}
|
|
}
|
|
// All routes on the list might be invalid
|
|
// Fault to the slow path that backtracks
|
|
// Or we want to do a strong host routing and haven't found the match
|
|
if ((pBestRoute == NULL) || (UnicastIf && (pCurrRoute == NULL))) {
|
|
return FindRTE(Dest, Source, 0, HOST_ROUTE_PRI, DEFAULT_ROUTE_PRI, UnicastIf);
|
|
}
|
|
return pBestRoute;
|
|
}
|
|
|
|
RouteTableEntry *
|
|
LookupForwardRTE(IPAddr Dest, IPAddr Source, BOOLEAN Multipath)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Searches for a best route for IP addr
|
|
on the forward path. If Multipath is
|
|
TRUE, it does a hash on the source and
|
|
dest addr. to pick one of the best
|
|
routes to the destination. This enables
|
|
the network to be utilized effectively
|
|
by providing load balancing.
|
|
|
|
Comments:
|
|
*LookupRTE* assumes that VALID flag
|
|
can be set on/off only for default
|
|
routes. Because in case we find a
|
|
chain with all invalid routes we do
|
|
not enough information to go up in
|
|
the F-trie for less specific routes
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Dest - Destination IP Addr
|
|
Source - Source IP Addr
|
|
Multipath - To do a equal cost multipath lookup or not
|
|
|
|
Return Value:
|
|
|
|
Matching RTE or NULL if not match
|
|
|
|
--*/
|
|
{
|
|
DestinationEntry *pBestDest;
|
|
RouteTableEntry *pBestRoute;
|
|
UINT hashValue;
|
|
UINT routeIndex;
|
|
UINT i;
|
|
|
|
// Try to match destination
|
|
pBestDest = SearchAddrInTrie(RouteTable, Dest);
|
|
|
|
// No prefix match - quit
|
|
if (pBestDest == NULL) {
|
|
return NULL;
|
|
}
|
|
// Search for a valid route
|
|
pBestRoute = pBestDest->firstRoute;
|
|
|
|
if (Multipath) {
|
|
KdPrintEx((DPFLTR_TCPIP_ID, DPFLTR_INFO_LEVEL,"\nIn Fwd RTE:\n Max = %d, Num = %d\n",
|
|
pBestDest->maxBestRoutes,
|
|
pBestDest->numBestRoutes));
|
|
|
|
// Get best route on dest from best routes' cache
|
|
|
|
if (pBestDest->numBestRoutes > 1) {
|
|
// Hash on the src, dest to get best route
|
|
hashValue = Source + Dest;
|
|
hashValue += (hashValue >> 16);
|
|
hashValue += (hashValue >> 8);
|
|
|
|
routeIndex = ((USHORT) hashValue) % pBestDest->numBestRoutes;
|
|
|
|
pBestRoute = pBestDest->bestRoutes[routeIndex];
|
|
|
|
KdPrintEx((DPFLTR_TCPIP_ID, DPFLTR_INFO_LEVEL,"S = %08x, D = %08x\nH = %08x, I = %d\nR = %p, N = %08x\n\n",
|
|
Source,
|
|
Dest,
|
|
hashValue,
|
|
routeIndex,
|
|
pBestRoute,
|
|
NHOP(pBestRoute)));
|
|
|
|
if ((FLAGS(pBestRoute) & RTE_VALID) && (!(FLAGS(pBestRoute) & RTE_DEADGW))) {
|
|
return pBestRoute;
|
|
}
|
|
}
|
|
// We do not want to match the source addr below
|
|
Source = NULL_IP_ADDR;
|
|
}
|
|
// Search for a valid route
|
|
pBestRoute = pBestDest->firstRoute;
|
|
|
|
while (pBestRoute) {
|
|
|
|
if ((FLAGS(pBestRoute) & RTE_VALID) &&
|
|
(!(FLAGS(pBestRoute) & RTE_DEADGW)))
|
|
break;
|
|
|
|
pBestRoute = NEXT(pBestRoute);
|
|
}
|
|
|
|
// All routes on the list might be invalid
|
|
// Fault to the slow path that backtracks
|
|
if (pBestRoute == NULL) {
|
|
return FindRTE(Dest, Source, 0, HOST_ROUTE_PRI, DEFAULT_ROUTE_PRI, 0);
|
|
}
|
|
return pBestRoute;
|
|
}
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Gets next route in the route-table.
|
|
|
|
The route returned is a Semi-Read-
|
|
Only-Version. The following fields
|
|
should be changed only by calling
|
|
the InsRoute function -
|
|
1) Next,
|
|
2) Dest,
|
|
3) Mask,
|
|
4) Priority, &
|
|
5) Route Metric.
|
|
|
|
Remaining fields can be changed by
|
|
directly modifying returned route.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Context - Iterator Context,
|
|
OUT -
|
|
ppRoute - To return route
|
|
|
|
Return Value:
|
|
|
|
TRUE if more routes, FALSE if not
|
|
|
|
--*/
|
|
|
|
UINT
|
|
GetNextRoute(VOID * Context, Route ** ppRoute)
|
|
{
|
|
UINT retVal;
|
|
|
|
// Get Next Route
|
|
retVal = IterateOverTrie(RouteTable, Context, ppRoute, NULL);
|
|
|
|
// We have routes
|
|
Assert(retVal != ERROR_TRIE_NO_ROUTES);
|
|
|
|
// Return Status
|
|
return (retVal == ERROR_TRIE_NOT_EMPTY) ? TRUE : FALSE;
|
|
}
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Enumerates all destinations in the route-table.
|
|
Assumes RouteTableLock is held by the caller.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
Context - iterator context, zeroed to begin enumeration.
|
|
OUT -
|
|
ppDest - receives enumerated destination, if any.
|
|
|
|
Return Value:
|
|
|
|
TRUE if more destinations, FALSE otherwise.
|
|
|
|
--*/
|
|
|
|
UINT
|
|
GetNextDest(VOID * Context, Dest ** ppDest)
|
|
{
|
|
UINT retVal;
|
|
|
|
// Get Next Destination
|
|
retVal = IterateOverTrie(RouteTable, Context, NULL, ppDest);
|
|
|
|
return (retVal == ERROR_TRIE_NOT_EMPTY) ? TRUE : FALSE;
|
|
}
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Re-orders all routes in a destination's route-list.
|
|
Assumes RouteTableLock is held by the caller.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
pDest - destination whose route-list is to be sorted
|
|
|
|
Return Value:
|
|
|
|
none.
|
|
|
|
--*/
|
|
|
|
VOID
|
|
SortRoutesInDest(Dest* pDest)
|
|
{
|
|
Route* pFirstRoute;
|
|
Route** ppCurrRoute;
|
|
|
|
// Pick up the current head of the route list, and replace it with NULL.
|
|
// We'll then rebuild the list by reinserting each item in order.
|
|
|
|
if (!(pFirstRoute = pDest->firstRoute)) {
|
|
return;
|
|
}
|
|
|
|
pDest->firstRoute = NULL;
|
|
|
|
while (pFirstRoute) {
|
|
Route* pNextRoute;
|
|
uint FirstOrder, CurrOrder;
|
|
|
|
if (FLAGS(pFirstRoute) & RTE_IF_VALID) {
|
|
FirstOrder = IF(pFirstRoute)->if_order;
|
|
} else {
|
|
FirstOrder = MAXLONG;
|
|
}
|
|
|
|
for (ppCurrRoute = &pDest->firstRoute; *ppCurrRoute;
|
|
ppCurrRoute = &NEXT(*ppCurrRoute)) {
|
|
if (FLAGS(*ppCurrRoute) & RTE_IF_VALID) {
|
|
CurrOrder = IF(*ppCurrRoute)->if_order;
|
|
} else {
|
|
CurrOrder = MAXLONG;
|
|
}
|
|
|
|
// N.B. The following sequence of comparisons ensure a *stable*
|
|
// sort, which is important to minimize the impact of this routine
|
|
// on ongoing sessions.
|
|
|
|
if (METRIC(pFirstRoute) > METRIC(*ppCurrRoute)) {
|
|
continue;
|
|
} else if (METRIC(pFirstRoute) < METRIC(*ppCurrRoute)) {
|
|
break;
|
|
}
|
|
|
|
if (FirstOrder < CurrOrder) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
pNextRoute = NEXT(pFirstRoute);
|
|
NEXT(pFirstRoute) = *ppCurrRoute;
|
|
*ppCurrRoute = pFirstRoute;
|
|
|
|
pFirstRoute = pNextRoute;
|
|
}
|
|
|
|
// Finally, reconstruct the array of best routes cached in the destination
|
|
|
|
if (pDest->firstRoute) {
|
|
CacheBestRoutesInDest(pDest);
|
|
}
|
|
}
|
|
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Re-orders all routes in the route-list of the destination
|
|
corresponding to a given route.
|
|
Assumes RouteTableLock is held by the caller.
|
|
|
|
Arguments:
|
|
|
|
IN -
|
|
pRTE - route whose destination's route-list is to be sorted
|
|
|
|
Return Value:
|
|
|
|
none.
|
|
|
|
--*/
|
|
|
|
VOID
|
|
SortRoutesInDestByRTE(Route *pRTE)
|
|
{
|
|
Dest* pDest = SearchAddrInTrie(RouteTable, DEST(pRTE));
|
|
if (pDest) {
|
|
SortRoutesInDest(pDest);
|
|
}
|
|
}
|
|
|
|
UINT
|
|
RTValidateContext(VOID * Context, UINT * Valid)
|
|
{
|
|
UINT retVal;
|
|
|
|
retVal = IsTrieIteratorValid(RouteTable, Context);
|
|
|
|
switch (retVal) {
|
|
case ERROR_TRIE_BAD_PARAM:
|
|
*Valid = FALSE;
|
|
return FALSE;
|
|
|
|
case ERROR_TRIE_NO_ROUTES:
|
|
*Valid = TRUE;
|
|
return FALSE;
|
|
|
|
case TRIE_SUCCESS:
|
|
*Valid = TRUE;
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|