You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
700 lines
16 KiB
700 lines
16 KiB
/********************************************************************/
|
|
/** Copyright(c) 1998 Microsoft Corporation. **/
|
|
/********************************************************************/
|
|
|
|
//***
|
|
//
|
|
// Filename: exports.c
|
|
|
|
//
|
|
// Description: Contains routines that are exported to the PPP engine. The
|
|
// engine calls into these routines for ATCP connections
|
|
//
|
|
// History: Feb 26, 1998 Shirish Koti Created original version.
|
|
//
|
|
//***
|
|
|
|
#include <nt.h>
|
|
#include <ntrtl.h>
|
|
#include <nturtl.h>
|
|
#include <windows.h>
|
|
|
|
#include <lmcons.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <llinfo.h>
|
|
#include <rasman.h>
|
|
#include <rtutils.h>
|
|
#include <devioctl.h>
|
|
#include <rasppp.h>
|
|
#include <pppcp.h>
|
|
#define INCL_HOSTWIRE
|
|
#include <ppputil.h>
|
|
#include <raserror.h>
|
|
|
|
#include <arapio.h>
|
|
#include "rasatcp.h"
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpGetInfo
|
|
// PPP engine calls this routine to get entry points into ATCP
|
|
//
|
|
// Parameters: dwProtocolId - unused here!
|
|
// pInfo - info that we fill out and pass back to PPP engine
|
|
//
|
|
// Return: NO_ERROR
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpGetInfo(
|
|
IN DWORD dwProtocolId,
|
|
OUT PPPCP_INFO *pInfo
|
|
)
|
|
{
|
|
|
|
ZeroMemory( pInfo, sizeof(PPPCP_INFO) );
|
|
|
|
pInfo->Protocol = (DWORD )PPP_ATCP_PROTOCOL;
|
|
lstrcpy(pInfo->SzProtocolName, "Atcp");
|
|
pInfo->Recognize = 7;
|
|
pInfo->RasCpInit = AtcpInit;
|
|
pInfo->RasCpBegin = AtcpBegin;
|
|
pInfo->RasCpReset = AtcpReset;
|
|
pInfo->RasCpEnd = AtcpEnd;
|
|
pInfo->RasCpThisLayerUp = AtcpThisLayerUp;
|
|
pInfo->RasCpMakeConfigRequest = AtcpMakeConfigRequest;
|
|
pInfo->RasCpMakeConfigResult = AtcpMakeConfigResult;
|
|
pInfo->RasCpConfigAckReceived = AtcpConfigAckReceived;
|
|
pInfo->RasCpConfigNakReceived = AtcpConfigNakReceived;
|
|
pInfo->RasCpConfigRejReceived = AtcpConfigRejReceived;
|
|
pInfo->RasCpGetNegotiatedInfo = AtcpGetNegotiatedInfo;
|
|
pInfo->RasCpProjectionNotification = AtcpProjectionNotification;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpInit
|
|
// PPP engine calls this routine to initialize ATCP
|
|
//
|
|
// Parameters: fInitialize - TRUE to initialize, FALSE to de-initialize
|
|
//
|
|
// Return: NO_ERROR if things go fine
|
|
// Errorcode if something fails
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpInit(
|
|
IN BOOL fInitialize
|
|
)
|
|
{
|
|
DWORD dwRetCode=NO_ERROR;
|
|
|
|
|
|
if (fInitialize)
|
|
{
|
|
// open handle to appletalk stack
|
|
if (!AtcpHandle)
|
|
{
|
|
atcpOpenHandle();
|
|
}
|
|
|
|
dwRetCode = atcpStartup();
|
|
}
|
|
else
|
|
{
|
|
// if we had a handle open to appletalk stack, close it
|
|
if (AtcpHandle)
|
|
{
|
|
atcpCloseHandle();
|
|
}
|
|
|
|
dwRetCode = atcpShutdown();
|
|
}
|
|
|
|
return(dwRetCode);
|
|
}
|
|
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpBegin
|
|
// PPP engine calls this routine to mark starting of a connection
|
|
// setup.
|
|
//
|
|
// Parameters: ppContext - context that we pass back
|
|
// pInfo - PPPCP_INIT info
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpBegin(
|
|
OUT PVOID *ppContext,
|
|
IN PVOID pInfo
|
|
)
|
|
{
|
|
|
|
DWORD dwRetCode;
|
|
PATCPCONN pAtcpConn=NULL;
|
|
|
|
|
|
*ppContext = NULL;
|
|
|
|
// open handle to stack if not already done so
|
|
if (!AtcpHandle)
|
|
{
|
|
atcpOpenHandle();
|
|
}
|
|
|
|
if (AtcpHandle == NULL)
|
|
{
|
|
ATCP_DBGPRINT(("atcpAtkSetup: AtcpHandle is NULL!\n"));
|
|
return(ARAPERR_IOCTL_FAILURE);
|
|
}
|
|
|
|
//
|
|
// allocate, initialize our connection context
|
|
//
|
|
if ((pAtcpConn = atcpAllocConnection((PPPCP_INIT *)pInfo)) == NULL)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpBegin: malloc failed\n"));
|
|
return(ERROR_NOT_ENOUGH_MEMORY);
|
|
}
|
|
|
|
//
|
|
// tell stack to allocate a context for this connection. Also, reserve a
|
|
// network addr for this client and get zone, router info
|
|
//
|
|
dwRetCode = atcpAtkSetup(pAtcpConn, IOCTL_ATCP_SETUP_CONNECTION);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpBegin: atcpAtkSetup failed %lx\n",dwRetCode));
|
|
LocalFree(pAtcpConn);
|
|
return(dwRetCode);
|
|
}
|
|
|
|
ATCP_DBGPRINT(("AtcpBegin: client's network addr %x.%x (%lx)\n",
|
|
pAtcpConn->ClientAddr.ata_Network,pAtcpConn->ClientAddr.ata_Node,pAtcpConn));
|
|
|
|
//
|
|
// allocate Route so we can call RasActivateRoute later...
|
|
//
|
|
dwRetCode = RasAllocateRoute(
|
|
pAtcpConn->hPort,
|
|
APPLETALK,
|
|
TRUE,
|
|
&pAtcpConn->RouteInfo);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpBegin: RasAllocateRoute failed %lx\n",dwRetCode));
|
|
|
|
// tell the stack to close the connection
|
|
atcpCloseAtalkConnection(pAtcpConn);
|
|
|
|
LocalFree(pAtcpConn);
|
|
return(dwRetCode);
|
|
}
|
|
|
|
*ppContext = (PVOID)pAtcpConn;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpThisLayerUp
|
|
// PPP engine calls this routine to tell us ATCP setup is done
|
|
//
|
|
// Parameters: pContext - our context
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpThisLayerUp(
|
|
IN PVOID pContext
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
BYTE ConfigInfo[ARAP_BIND_SIZE];
|
|
PROTOCOL_CONFIG_INFO *pCfgInfo;
|
|
PARAP_BIND_INFO pBindInfo;
|
|
DWORD dwRetCode;
|
|
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
EnterCriticalSection(&pAtcpConn->CritSect);
|
|
if (pAtcpConn->fLineUpDone)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpThisLayerUp: LineUp already done\n"));
|
|
LeaveCriticalSection(&pAtcpConn->CritSect);
|
|
return(NO_ERROR);
|
|
}
|
|
|
|
pAtcpConn->fLineUpDone = TRUE;
|
|
|
|
LeaveCriticalSection(&pAtcpConn->CritSect);
|
|
|
|
if (pAtcpConn->SuppressRtmp || pAtcpConn->SuppressAllBcast)
|
|
{
|
|
atcpAtkSetup(pAtcpConn, IOCTL_ATCP_SUPPRESS_BCAST);
|
|
}
|
|
|
|
pCfgInfo = (PROTOCOL_CONFIG_INFO *)ConfigInfo;
|
|
pBindInfo = (PARAP_BIND_INFO)&pCfgInfo->P_Info[0];
|
|
pCfgInfo->P_Length = ARAP_BIND_SIZE;
|
|
|
|
//
|
|
// plumb our protocol-specific info
|
|
//
|
|
pBindInfo->BufLen = sizeof( ARAP_BIND_INFO );
|
|
pBindInfo->pDllContext = pAtcpConn;
|
|
pBindInfo->fThisIsPPP = TRUE;
|
|
pBindInfo->ClientAddr = pAtcpConn->ClientAddr;
|
|
pBindInfo->AtalkContext = pAtcpConn->AtalkContext;
|
|
pBindInfo->ErrorCode = (DWORD)-1;
|
|
|
|
//
|
|
// pass it down to ndiswan so our stack gets a line-up!
|
|
//
|
|
dwRetCode = RasActivateRoute(
|
|
pAtcpConn->hPort,
|
|
APPLETALK,
|
|
&pAtcpConn->RouteInfo,
|
|
pCfgInfo);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpProjectionNotification: RasActivateRoute failed %lx\n",
|
|
dwRetCode));
|
|
|
|
return(dwRetCode);
|
|
}
|
|
|
|
ATCP_DBGPRINT(("AtcpThisLayerUp: LineUp done on %lx\n",pAtcpConn));
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpMakeConfigRequest
|
|
// PPP engine calls this routine to ask us to prepare an
|
|
// ATCP ConfigRequest packet
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pSendBuf - PPP_CONFIG info for this request
|
|
// cbSendBuf - how big is the Data buffer
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpMakeConfigRequest(
|
|
IN PVOID pContext,
|
|
OUT PPP_CONFIG *pSendBuf,
|
|
IN DWORD cbSendBuf
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
DWORD dwRetCode;
|
|
USHORT OptionType;
|
|
BOOL fConfigDone=FALSE;
|
|
BYTE ParseResult[ATCP_OPT_MAX_VAL];
|
|
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
EnterCriticalSection(&pAtcpConn->CritSect);
|
|
if (pAtcpConn->Flags & ATCP_CONFIG_REQ_DONE)
|
|
{
|
|
fConfigDone = TRUE;
|
|
}
|
|
LeaveCriticalSection(&pAtcpConn->CritSect);
|
|
|
|
if (fConfigDone)
|
|
{
|
|
pSendBuf->Code = CONFIG_REQ;
|
|
HostToWireFormat16(4, pSendBuf->Length );
|
|
ATCP_DBGPRINT(("AtcpMakeConfigRequest: our-side config done, returning\n"));
|
|
return(NO_ERROR);
|
|
}
|
|
|
|
// initialize everything to not-needed
|
|
for (OptionType=1; OptionType<ATCP_OPT_MAX_VAL; OptionType++ )
|
|
{
|
|
ParseResult[OptionType] = ATCP_NOT_REQUESTED;
|
|
}
|
|
|
|
// set the ones we want
|
|
ParseResult[ATCP_OPT_APPLETALK_ADDRESS] = ATCP_REQ;
|
|
ParseResult[ATCP_OPT_SERVER_INFORMATION] = ATCP_REQ;
|
|
ParseResult[ATCP_OPT_ZONE_INFORMATION] = ATCP_REQ;
|
|
ParseResult[ATCP_OPT_DEFAULT_ROUTER_ADDRESS] = ATCP_REQ;
|
|
|
|
// prepare our ConfigRequest
|
|
dwRetCode = atcpPrepareResponse(
|
|
pAtcpConn,
|
|
pSendBuf,
|
|
cbSendBuf,
|
|
ParseResult);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpMakeConfigRequest: atcpPrepareResponse failed %lx\n",
|
|
dwRetCode));
|
|
return(dwRetCode);
|
|
}
|
|
|
|
return(NO_ERROR);
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpMakeConfigResult
|
|
// PPP engine calls this routine to ask us to prepare a response:
|
|
// ConfigAck, ConfigNak or ConfigReject
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pReceiveBuf - PPP_CONFIG info: the request
|
|
// pSendBuf - PPP_CONFIG info: our response
|
|
// cbSendBuf - how big is the Data buffer for our response
|
|
// fRejectNaks - if TRUE, Reject an option instead of Nak'ing it
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpMakeConfigResult(
|
|
IN PVOID pContext,
|
|
IN PPP_CONFIG *pReceiveBuf,
|
|
OUT PPP_CONFIG *pSendBuf,
|
|
IN DWORD cbSendBuf,
|
|
IN BOOL fRejectNaks
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
DWORD dwRetCode;
|
|
BYTE ParseResult[ATCP_OPT_MAX_VAL+1];
|
|
BOOL fRejectingSomething=FALSE;
|
|
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
//
|
|
// parse this request.
|
|
//
|
|
dwRetCode = atcpParseRequest(
|
|
pAtcpConn,
|
|
pReceiveBuf,
|
|
pSendBuf,
|
|
cbSendBuf,
|
|
ParseResult,
|
|
&fRejectingSomething);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpMakeConfigResult: atcpParseRequest failed %lx\n",
|
|
dwRetCode));
|
|
return(dwRetCode);
|
|
}
|
|
|
|
//
|
|
// If some option needs to be rejected, the outgoing buffer already
|
|
// contains the appropriate stuff: just return here
|
|
//
|
|
if (fRejectingSomething)
|
|
{
|
|
return(NO_ERROR);
|
|
}
|
|
|
|
//
|
|
// we are not rejecting any option. Prepare a response buffer to send
|
|
//
|
|
dwRetCode = atcpPrepareResponse(
|
|
pAtcpConn,
|
|
pSendBuf,
|
|
cbSendBuf,
|
|
ParseResult);
|
|
|
|
if (dwRetCode != NO_ERROR)
|
|
{
|
|
ATCP_DBGPRINT(("AtcpMakeConfigResult: atcpPrepareResponse failed %lx\n",
|
|
dwRetCode));
|
|
return(dwRetCode);
|
|
}
|
|
|
|
return(NO_ERROR);
|
|
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpConfigAckReceived
|
|
// PPP engine calls this routine to tell us that we got ConfigAck
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pReceiveBuf - PPP_CONFIG info: the ack
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpConfigAckReceived(
|
|
IN PVOID pContext,
|
|
IN PPP_CONFIG *pReceiveBuf
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
// client is happy with our-side configuration
|
|
EnterCriticalSection(&pAtcpConn->CritSect);
|
|
pAtcpConn->Flags |= ATCP_CONFIG_REQ_DONE;
|
|
LeaveCriticalSection(&pAtcpConn->CritSect);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpConfigNakReceived
|
|
// PPP engine calls this routine to tell us that we got ConfigNak
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pReceiveBuf - PPP_CONFIG info: the ack
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpConfigNakReceived(
|
|
IN PVOID pContext,
|
|
IN PPP_CONFIG *pReceiveBuf
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
ATCP_DBGPRINT(("AtcpConfigNakReceived entered\n"));
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
ATCP_DUMP_BYTES("AtcpConfigNakReceived: Nak received from client",
|
|
&pReceiveBuf->Data[0],
|
|
(DWORD)WireToHostFormat16( pReceiveBuf->Length ) - 4);
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpConfigRejReceived
|
|
// PPP engine calls this routine to tell us that we got ConfigRej
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pReceiveBuf - PPP_CONFIG info: the ack
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpConfigRejReceived(
|
|
IN PVOID pContext,
|
|
IN PPP_CONFIG *pReceiveBuf
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
ATCP_DBGPRINT(("AtcpConfigRejReceived entered\n"));
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpGetNegotiatedInfo
|
|
// PPP engine calls this routine to retrieve from us the info that
|
|
// finally got negotiated.
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pReceiveBuf - PPP_CONFIG info: the ack
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpGetNegotiatedInfo(
|
|
IN PVOID pContext,
|
|
OUT PPP_ATCP_RESULT *pAtcpResult
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
pAtcpResult->dwError = 0;
|
|
|
|
pAtcpResult->dwLocalAddress = *(DWORD *)&AtcpServerAddress;
|
|
pAtcpResult->dwRemoteAddress = *(DWORD *)&pAtcpConn->ClientAddr;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpProjectionNotification
|
|
// PPP engine calls this routine to tell us that all CPs have been
|
|
// negotiated.
|
|
//
|
|
// Parameters: pContext - our context
|
|
// pProjectionResult - PPP_PROJECTION_RESULT info
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpProjectionNotification(
|
|
IN PVOID pContext,
|
|
IN PVOID pProjectionResult
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
|
|
ATCP_DBGPRINT(("AtcpProjectionNotification entered\n"));
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpReset
|
|
// Don't know when/why PPP engine calls this routine: this routine
|
|
// just returns success!
|
|
//
|
|
// Parameters: pContext - our context
|
|
//
|
|
// Return: always 0
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpReset(
|
|
IN PVOID pContext
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
ATCP_DBGPRINT(("AtcpReset entered\n"));
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
//***
|
|
//
|
|
// Function: AtcpEnd
|
|
// PPP engine calls this routine to mark end of a connection.
|
|
//
|
|
// Parameters: pContext - our context
|
|
//
|
|
// Return: result of the operation
|
|
//
|
|
//***$
|
|
|
|
DWORD
|
|
AtcpEnd(
|
|
IN PVOID pContext
|
|
)
|
|
{
|
|
PATCPCONN pAtcpConn;
|
|
|
|
ATCP_DBGPRINT(("AtcpEnd entered\n"));
|
|
|
|
pAtcpConn = (PATCPCONN)pContext;
|
|
|
|
ATCP_ASSERT(pAtcpConn->Signature == ATCP_SIGNATURE);
|
|
|
|
// tell the stack to close the connection
|
|
atcpCloseAtalkConnection(pAtcpConn);
|
|
|
|
// deactivate the ras route so stack gets a line-down
|
|
RasDeAllocateRoute(pAtcpConn->hConnection, APPLETALK);
|
|
|
|
if (pAtcpConn->fCritSectInitialized)
|
|
{
|
|
DeleteCriticalSection( &pAtcpConn->CritSect );
|
|
pAtcpConn->fCritSectInitialized = FALSE;
|
|
}
|
|
|
|
#if DBG
|
|
// mess up the memory so we can catch bad things (using free'd memory etc.)
|
|
memset( pAtcpConn, 'f', sizeof(ATCPCONN) );
|
|
pAtcpConn->Signature = 0xDEADBEEF;
|
|
#endif
|
|
|
|
LocalFree(pAtcpConn);
|
|
|
|
EnterCriticalSection( &AtcpCritSect );
|
|
AtcpNumConnections--;
|
|
LeaveCriticalSection( &AtcpCritSect );
|
|
|
|
return 0;
|
|
}
|
|
|
|
|