686 lines
16 KiB
C
686 lines
16 KiB
C
|
//============================================================================
|
||
|
// Copyright (c) 1995, Microsoft Corporation
|
||
|
//
|
||
|
// File: Upgrade.c
|
||
|
//
|
||
|
// History:
|
||
|
// V Raman July-1-1997 Created.
|
||
|
//
|
||
|
// Entry points for ISDN upgrade
|
||
|
//============================================================================
|
||
|
|
||
|
|
||
|
#include <nt.h>
|
||
|
#include <ntrtl.h>
|
||
|
#include <nturtl.h>
|
||
|
#include <windows.h>
|
||
|
#include <setupapi.h>
|
||
|
#include <tchar.h>
|
||
|
#include <stdio.h>
|
||
|
#include <string.h>
|
||
|
|
||
|
#include "upgrade.h"
|
||
|
|
||
|
//
|
||
|
// upgrade inf section name
|
||
|
//
|
||
|
|
||
|
|
||
|
#define MAX_CHARS_PER_LINE 128
|
||
|
|
||
|
#define ALIGN_SIZE 0x00000008
|
||
|
#define ALIGN_SHIFT (ALIGN_SIZE - 0x00000001) // 0x00000007
|
||
|
#define ALIGN_MASK (~ALIGN_SHIFT) // 0xfffffff8
|
||
|
|
||
|
#define ALIGN_POINTER(ptr) { \
|
||
|
((DWORD) (ptr)) += ALIGN_SHIFT; \
|
||
|
((DWORD) (ptr)) &= ALIGN_MASK; \
|
||
|
}
|
||
|
|
||
|
|
||
|
typedef struct _ISDN_STATIC_PARAMS
|
||
|
{
|
||
|
WCHAR ptszInfId[ MAX_PATH ];
|
||
|
DWORD dwWanEndPoints;
|
||
|
DWORD dwNumDChannels;
|
||
|
DWORD dwNumBChannelsPerDChannel;
|
||
|
|
||
|
} ISDN_STATIC_PARAMS, *PISDN_STATIC_PARAMS;
|
||
|
|
||
|
|
||
|
|
||
|
//
|
||
|
// New inf Key names
|
||
|
//
|
||
|
|
||
|
WCHAR c_ptszIsdnNumDChannels[] = L"IsdnNumDChannels";
|
||
|
WCHAR c_ptszIsdnSwitchType[] = L"IsdnSwitchType";
|
||
|
|
||
|
WCHAR c_ptszIsdnNumBChannels[] = L"IsdnNumBChannels";
|
||
|
WCHAR c_ptszIsdnSpid[] = L"IsdnSpid";
|
||
|
WCHAR c_ptszIsdnPhoneNumber[] = L"IsdnPhoneNumber";
|
||
|
|
||
|
|
||
|
//============================================================================
|
||
|
// Values for DIGI Datafire ISDN cards.
|
||
|
//
|
||
|
//
|
||
|
//============================================================================
|
||
|
|
||
|
#define DIGI_MAX_ADAPTERS 3
|
||
|
#define DIGI_MAX_SWITCH_TYPES 12
|
||
|
|
||
|
ISDN_STATIC_PARAMS g_ispDigiParams[] =
|
||
|
{
|
||
|
{ TEXT( "DataFireIsa1U" ), 2, 1, 2 },
|
||
|
{ L "DataFireIsa1ST" , 2, 1, 2 },
|
||
|
{ L "DataFireIsa4ST" , 8, 4, 2 }
|
||
|
};
|
||
|
|
||
|
|
||
|
PWSTR g_ptszDigiSwitchTypes[] =
|
||
|
{
|
||
|
L "Auto Detect" ,
|
||
|
L "Generic" ,
|
||
|
L "ATT" ,
|
||
|
L "DEFINITY" ,
|
||
|
L "NTI" ,
|
||
|
L "NI1" ,
|
||
|
L "NET3" ,
|
||
|
L "1TR6" ,
|
||
|
L "VN3" ,
|
||
|
L "INS64" ,
|
||
|
L "AUSTEL" ,
|
||
|
L "Singapore"
|
||
|
};
|
||
|
|
||
|
|
||
|
//
|
||
|
// registry keys for DIGI Datafire ISDN card.
|
||
|
//
|
||
|
|
||
|
WCHAR c_ptszNumberOfLines[] = L "NumberOfLines" ;
|
||
|
|
||
|
WCHAR c_ptszLine[] = L "Line" ;
|
||
|
WCHAR c_ptszSwitchStyle[] = L "SwitchStyle" ;
|
||
|
WCHAR c_ptszLogicalTerminals[] = L "LogicalTerminals" ;
|
||
|
WCHAR c_ptszTerminalManagement[] = L "TerminalManagement" ;
|
||
|
|
||
|
WCHAR c_ptszLTerm[] = L "LTerm" ;
|
||
|
WCHAR c_ptszSPID[] = L "SPID" ;
|
||
|
WCHAR c_ptszAddress[] = L "Address" ;
|
||
|
|
||
|
|
||
|
|
||
|
//----------------------------------------------------------------------------
|
||
|
// Function: DLLMAIN
|
||
|
//
|
||
|
// This is the DLL entrypoint handler.
|
||
|
//----------------------------------------------------------------------------
|
||
|
|
||
|
BOOL
|
||
|
WINAPI
|
||
|
DLLMAIN(
|
||
|
HINSTANCE hInstance,
|
||
|
DWORD dwReason,
|
||
|
PVOID pUnused
|
||
|
)
|
||
|
{
|
||
|
|
||
|
switch(dwReason) {
|
||
|
case DLL_PROCESS_ATTACH:
|
||
|
|
||
|
DisableThreadLibraryCalls(hInstance);
|
||
|
|
||
|
break;
|
||
|
|
||
|
case DLL_PROCESS_DETACH:
|
||
|
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
//----------------------------------------------------------------------------
|
||
|
// Function: NetWriteDIGIISDNRegistry
|
||
|
//
|
||
|
// reads the registry layout of DIGI PCIMAC card and write the key
|
||
|
// values that need to be updated in the 5.0 setup.
|
||
|
//----------------------------------------------------------------------------
|
||
|
|
||
|
DWORD
|
||
|
NetWriteDIGIISDNRegistry(
|
||
|
IN HKEY hKeyInstanceParameters,
|
||
|
IN PCWSTR szPreNT5IndId,
|
||
|
IN PCWSTR szNT5InfId,
|
||
|
IN PCWSTR szSectionName,
|
||
|
OUT PWSTR ** lplplpBuffer,
|
||
|
IN OUT PDWORD pdwNumLines
|
||
|
)
|
||
|
{
|
||
|
|
||
|
DWORD dwInd = 0, dwInd1 = 0, dwErr = ERROR_SUCCESS;
|
||
|
|
||
|
DWORD dwMaxKeyLen = 0, dwMaxValueLen = 0, dwType = 0, dwSize = 0,
|
||
|
dwNumSubKeys = 0;
|
||
|
|
||
|
DWORD dwValue = 0, dwNumDChannels = 0, dwNumLogTerm = 0;
|
||
|
DWORD dwMaxLines = 0, dwNumLines = 0;
|
||
|
|
||
|
PTCHAR ptszKey = NULL, ptszValue = NULL;
|
||
|
|
||
|
PTCHAR ptszBuffer = NULL;
|
||
|
|
||
|
WCHAR ptszSubKey[ MAX_PATH ];
|
||
|
|
||
|
PBYTE pb = NULL, pbLine = NULL;
|
||
|
|
||
|
PWSTR * lplpOffset;
|
||
|
|
||
|
PISDN_STATIC_PARAMS pisp = NULL;
|
||
|
|
||
|
|
||
|
|
||
|
do
|
||
|
{
|
||
|
|
||
|
//
|
||
|
// Create IsdnNumDChannel entry
|
||
|
//
|
||
|
// for each D channel
|
||
|
// Create IsdnSwitchType (index into the mulptsz)
|
||
|
// Create VendorSpecific\TerminalManagement
|
||
|
// Create VendorSpecific\LogicalTerminals
|
||
|
//
|
||
|
// For each B Channel
|
||
|
// Create IsdnSpid
|
||
|
// Create IsdnPhoneNumber
|
||
|
//
|
||
|
|
||
|
|
||
|
//
|
||
|
// figure out which card this is
|
||
|
//
|
||
|
|
||
|
|
||
|
for ( dwInd = 0; dwInd < DIGI_MAX_ADAPTERS; dwInd++ )
|
||
|
{
|
||
|
if ( !_wcsicmp( g_ispDigiParams[ dwInd ].ptszInfId, szNT5InfId ) )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( dwInd >= DIGI_MAX_ADAPTERS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
pisp = &(g_ispDigiParams[ dwInd ]);
|
||
|
|
||
|
|
||
|
|
||
|
// Compute number of lines in buffer as
|
||
|
//
|
||
|
// 4 (section name, Addreg line, blank line, Addreg section name) +
|
||
|
// 1 (IsdnNumDChannels) +
|
||
|
// IsdnNumDChannels * ( 3 +
|
||
|
// ( IsdnNumBChannels * 2 )
|
||
|
//
|
||
|
|
||
|
dwMaxLines = 5 +
|
||
|
( pisp-> dwNumDChannels *
|
||
|
( 3 + pisp-> dwNumBChannelsPerDChannel * 2 ) );
|
||
|
|
||
|
|
||
|
|
||
|
//
|
||
|
// allocate buffer big enough to hold these many lines of entries
|
||
|
//
|
||
|
|
||
|
dwSize = sizeof( PWSTR ) * dwMaxLines + ALIGN_SIZE +
|
||
|
dwMaxLines * sizeof( WCHAR ) * MAX_CHARS_PER_LINE;
|
||
|
|
||
|
|
||
|
pb = LocalAlloc( 0, dwSize );
|
||
|
|
||
|
if ( pb == NULL )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
ZeroMemory( pb, dwSize );
|
||
|
|
||
|
|
||
|
lplpOffset = (PWSTR *) pb;
|
||
|
|
||
|
pbLine = pb + sizeof( PWSTR ) * dwMaxLines;
|
||
|
|
||
|
ALIGN_POINTER( pbLine );
|
||
|
|
||
|
|
||
|
//
|
||
|
// set the pointers
|
||
|
//
|
||
|
|
||
|
for ( dwInd = 0; dwInd < dwMaxLines; dwInd++ )
|
||
|
{
|
||
|
lplpOffset[ dwInd ] = (PWSTR) ( pbLine +
|
||
|
dwInd * sizeof( WCHAR ) * MAX_CHARS_PER_LINE );
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// write section name
|
||
|
//
|
||
|
|
||
|
dwNumLines = 0;
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
wsprintfW( ptszBuffer, L"[%s]", szSectionName );
|
||
|
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L"AddReg\t= %s.%s.reg",
|
||
|
szSectionName,
|
||
|
szNT5InfId
|
||
|
);
|
||
|
|
||
|
|
||
|
//
|
||
|
// Start registry section name
|
||
|
//
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L"[%s.%s.reg]",
|
||
|
szSectionName,
|
||
|
szNT5InfId
|
||
|
);
|
||
|
|
||
|
|
||
|
//
|
||
|
// Find size of longest sub key and the size of max value
|
||
|
// Allocate buffers for them.
|
||
|
//
|
||
|
|
||
|
dwErr = RegQueryInfoKey(
|
||
|
hKeyInstanceParameters,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
NULL,
|
||
|
&dwMaxKeyLen,
|
||
|
&dwMaxValueLen,
|
||
|
NULL,
|
||
|
NULL
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
dwErr = GetLastError();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
|
||
|
ptszKey = HeapAlloc(
|
||
|
GetProcessHeap(),
|
||
|
0,
|
||
|
(dwMaxKeyLen + 1) * sizeof( WCHAR )
|
||
|
);
|
||
|
|
||
|
if ( ptszKey == NULL )
|
||
|
{
|
||
|
dwErr = ERROR_NOT_ENOUGH_MEMORY;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
ptszValue = HeapAlloc(
|
||
|
GetProcessHeap(),
|
||
|
0,
|
||
|
(dwMaxValueLen + 1) * sizeof( WCHAR )
|
||
|
);
|
||
|
|
||
|
if ( ptszValue == NULL )
|
||
|
{
|
||
|
dwErr = ERROR_NOT_ENOUGH_MEMORY;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// Find number of D channels
|
||
|
//
|
||
|
|
||
|
dwSize = sizeof( DWORD );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
c_ptszNumberOfLines,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(PBYTE) &dwNumDChannels,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L "HKR, , %s, %#.8lx, %d" ,
|
||
|
c_ptszIsdnNumDChannels,
|
||
|
FLG_ADDREG_TYPE_DWORD,
|
||
|
dwNumDChannels
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
|
||
|
//
|
||
|
// for each D channel
|
||
|
//
|
||
|
|
||
|
for ( dwInd = 0; dwInd < dwNumDChannels; dwInd++ )
|
||
|
{
|
||
|
//
|
||
|
// Sub key for D channels under which all the info
|
||
|
// is to written
|
||
|
//
|
||
|
|
||
|
wsprintfW( ptszSubKey, L"%d" , dwInd );
|
||
|
|
||
|
|
||
|
//
|
||
|
// create switch type key name
|
||
|
//
|
||
|
|
||
|
wsprintfW( ptszKey, L"%s%d.%s" , c_ptszLine, dwInd, c_ptszSwitchStyle );
|
||
|
|
||
|
|
||
|
//
|
||
|
// query protocol key and use look up to determine switch type.
|
||
|
//
|
||
|
|
||
|
dwSize = dwMaxValueLen * sizeof( WCHAR );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
ptszKey,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE) ptszValue,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// lookup the switchtype in the list of switch types
|
||
|
//
|
||
|
|
||
|
for ( dwInd1 = 0; dwInd1 < DIGI_MAX_SWITCH_TYPES; dwInd1++ )
|
||
|
{
|
||
|
if ( !_wcsicmp( g_ptszDigiSwitchTypes[ dwInd1 ], ptszValue ) )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( dwInd1 >= DIGI_MAX_SWITCH_TYPES )
|
||
|
{
|
||
|
dwInd1 = 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L "HKR, %s, %s, %#.8lx, %d" ,
|
||
|
ptszSubKey,
|
||
|
c_ptszIsdnSwitchType,
|
||
|
FLG_ADDREG_TYPE_DWORD,
|
||
|
dwInd1
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
|
||
|
//
|
||
|
// Create key for number of B channels
|
||
|
//
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszKey,
|
||
|
L"%s%d.%s" ,
|
||
|
c_ptszLine, dwInd, c_ptszLogicalTerminals
|
||
|
);
|
||
|
|
||
|
|
||
|
//
|
||
|
// Find and write number of logical terminals
|
||
|
//
|
||
|
|
||
|
dwSize = sizeof( DWORD );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
ptszKey,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE) &dwNumLogTerm,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L "HKR, %s\\VendorSpecific, %s, %#.8lx, %d" ,
|
||
|
ptszSubKey,
|
||
|
c_ptszLogicalTerminals,
|
||
|
FLG_ADDREG_TYPE_DWORD,
|
||
|
dwNumLogTerm
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
|
||
|
//
|
||
|
// Create key for Terminal management
|
||
|
//
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszKey,
|
||
|
L"%s%d.%s" ,
|
||
|
c_ptszLine, dwInd, c_ptszTerminalManagement
|
||
|
);
|
||
|
|
||
|
|
||
|
//
|
||
|
// Find and write terminal management value
|
||
|
//
|
||
|
|
||
|
dwSize = dwMaxValueLen * sizeof( WCHAR );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
ptszKey,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE) ptszValue,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L "HKR, %s\\VendorSpecific, %s, %#.8lx, %s" ,
|
||
|
ptszSubKey,
|
||
|
c_ptszTerminalManagement,
|
||
|
FLG_ADDREG_TYPE_SZ,
|
||
|
ptszValue
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
|
||
|
//
|
||
|
// for each B Channel for this D channels write the
|
||
|
// B channel info.
|
||
|
//
|
||
|
|
||
|
for ( dwInd1 = 0; dwInd1 < pisp-> dwNumBChannelsPerDChannel; dwInd1++ )
|
||
|
{
|
||
|
//
|
||
|
// sub key for B channel under which all the info. is to
|
||
|
// be written.
|
||
|
//
|
||
|
|
||
|
wsprintfW( ptszSubKey, L"%d\\%d" , dwInd, dwInd1 );
|
||
|
|
||
|
|
||
|
//
|
||
|
// create key for SPID
|
||
|
//
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszKey, L"%s%d.%s%d.%s" ,
|
||
|
c_ptszLine, dwInd, c_ptszLTerm, dwInd1, c_ptszSPID
|
||
|
);
|
||
|
|
||
|
//
|
||
|
// retrieve and write value of SPID key
|
||
|
//
|
||
|
|
||
|
dwSize = dwMaxValueLen * sizeof( WCHAR );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
ptszKey,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE) ptszValue,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
dwErr = ERROR_SUCCESS;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L "HKR, %s, %s, %#.8lx, %s" ,
|
||
|
ptszSubKey,
|
||
|
c_ptszIsdnSpid,
|
||
|
FLG_ADDREG_TYPE_SZ,
|
||
|
ptszValue
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
|
||
|
|
||
|
//
|
||
|
// create key for Phone Number
|
||
|
//
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszKey, L"%s%d.%s%d.%s" ,
|
||
|
c_ptszLine, dwInd, c_ptszLTerm, dwInd1, c_ptszAddress
|
||
|
);
|
||
|
|
||
|
//
|
||
|
// retrieve and write value of phone number key
|
||
|
//
|
||
|
|
||
|
dwSize = dwMaxValueLen * sizeof( WCHAR );
|
||
|
|
||
|
dwErr = RegQueryValueEx(
|
||
|
hKeyInstanceParameters,
|
||
|
ptszKey,
|
||
|
NULL,
|
||
|
&dwType,
|
||
|
(LPBYTE) ptszValue,
|
||
|
&dwSize
|
||
|
);
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
dwErr = ERROR_SUCCESS;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
|
||
|
wsprintfW(
|
||
|
ptszBuffer,
|
||
|
L"HKR, %s, %s, %#.8lx, %s" ,
|
||
|
ptszSubKey,
|
||
|
c_ptszIsdnPhoneNumber,
|
||
|
FLG_ADDREG_TYPE_SZ,
|
||
|
ptszValue
|
||
|
);
|
||
|
|
||
|
ptszBuffer = lplpOffset[ dwNumLines++ ];
|
||
|
}
|
||
|
|
||
|
if ( dwErr != ERROR_SUCCESS )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
} while ( FALSE );
|
||
|
|
||
|
|
||
|
//
|
||
|
// clean up
|
||
|
//
|
||
|
|
||
|
if ( ptszKey ) { HeapFree( GetProcessHeap(), 0, ptszKey ); }
|
||
|
|
||
|
if ( ptszValue ) { HeapFree( GetProcessHeap(), 0, ptszValue ); }
|
||
|
|
||
|
|
||
|
if ( dwErr == ERROR_SUCCESS )
|
||
|
{
|
||
|
*lplplpBuffer =(PWSTR *) pb;
|
||
|
*pdwNumLines = dwNumLines;
|
||
|
}
|
||
|
|
||
|
return dwErr;
|
||
|
}
|
||
|
|
||
|
|