692 lines
12 KiB
C
692 lines
12 KiB
C
|
/*++
|
||
|
|
||
|
Copyright (c) 1997 Microsoft Corporation
|
||
|
|
||
|
Module Name:
|
||
|
|
||
|
utils.c
|
||
|
|
||
|
Abstract:
|
||
|
|
||
|
This source file implements utility functions used by scrnsave.c.
|
||
|
|
||
|
Author:
|
||
|
|
||
|
Jim Schmidt (jimschm) 11-Apr-1997
|
||
|
|
||
|
Revision History:
|
||
|
|
||
|
|
||
|
--*/
|
||
|
|
||
|
#include "pch.h"
|
||
|
|
||
|
#ifdef UNICODE
|
||
|
#error UNICODE cannot be defined
|
||
|
#endif
|
||
|
|
||
|
//
|
||
|
// Declare strings
|
||
|
//
|
||
|
|
||
|
#define DEFMAC(var,str) CHAR var[] = str;
|
||
|
DEFINE_STRINGS
|
||
|
#undef DEFMAC
|
||
|
|
||
|
//
|
||
|
// Temporary buffer
|
||
|
//
|
||
|
|
||
|
static CHAR g_Data[MAX_PATH];
|
||
|
|
||
|
//
|
||
|
// Buffer for string representation of registry keys (for error logging)
|
||
|
//
|
||
|
|
||
|
typedef struct _tagKEYTOSTR {
|
||
|
struct _tagKEYTOSTR *Prev, *Next;
|
||
|
HKEY Key;
|
||
|
CHAR RegKey[];
|
||
|
} KEYTOSTR, *PKEYTOSTR;
|
||
|
|
||
|
static PKEYTOSTR g_Head = NULL;
|
||
|
|
||
|
VOID
|
||
|
pAddKeyToStrMapping (
|
||
|
IN HKEY Key,
|
||
|
IN LPCSTR RootStr,
|
||
|
IN LPCSTR KeyStr
|
||
|
)
|
||
|
{
|
||
|
PKEYTOSTR Node;
|
||
|
DWORD Size;
|
||
|
CHAR FullKeyStr[MAX_PATH];
|
||
|
|
||
|
// We control RootStr and KeyStr, so we know it is less than MAX_PATH in length
|
||
|
wsprintf (FullKeyStr, "%s\\%s", RootStr, KeyStr);
|
||
|
|
||
|
Size = sizeof (KEYTOSTR) + CountStringBytes (FullKeyStr);
|
||
|
|
||
|
Node = (PKEYTOSTR) HeapAlloc (g_hHeap, 0, Size);
|
||
|
if (Node) {
|
||
|
Node->Prev = NULL;
|
||
|
Node->Next = g_Head;
|
||
|
Node->Key = Key;
|
||
|
_mbscpy (Node->RegKey, FullKeyStr);
|
||
|
|
||
|
if (g_Head) {
|
||
|
g_Head->Prev = Node;
|
||
|
}
|
||
|
g_Head = Node;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
PKEYTOSTR
|
||
|
pFindKeyToStrMapping (
|
||
|
IN HKEY Key
|
||
|
)
|
||
|
{
|
||
|
PKEYTOSTR Node;
|
||
|
|
||
|
Node = g_Head;
|
||
|
while (Node) {
|
||
|
if (Node->Key == Key) {
|
||
|
return Node;
|
||
|
}
|
||
|
Node = Node->Next;
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
VOID
|
||
|
pRemoveKeyToStrMapping (
|
||
|
IN HKEY Key
|
||
|
)
|
||
|
{
|
||
|
PKEYTOSTR Node;
|
||
|
|
||
|
Node = pFindKeyToStrMapping (Key);
|
||
|
if (!Node) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (Node->Prev) {
|
||
|
Node->Prev->Next = Node->Next;
|
||
|
} else {
|
||
|
g_Head = Node->Next;
|
||
|
}
|
||
|
|
||
|
if (Node->Next) {
|
||
|
Node->Next->Prev = Node->Prev;
|
||
|
}
|
||
|
|
||
|
HeapFree (g_hHeap, 0, Node);
|
||
|
}
|
||
|
|
||
|
|
||
|
VOID
|
||
|
LogRegistryError (
|
||
|
IN HKEY Key,
|
||
|
IN LPCSTR ValueName
|
||
|
)
|
||
|
{
|
||
|
DWORD rc = GetLastError();
|
||
|
LPCSTR FullKeyStr;
|
||
|
PKEYTOSTR Node;
|
||
|
|
||
|
Node = pFindKeyToStrMapping (Key);
|
||
|
if (Node) {
|
||
|
FullKeyStr = Node->RegKey;
|
||
|
} else {
|
||
|
FullKeyStr = S_DEFAULT_KEYSTR;
|
||
|
}
|
||
|
|
||
|
LOG ((LOG_ERROR, MSG_REGISTRY_ERROR, g_User, rc, FullKeyStr, ValueName));
|
||
|
}
|
||
|
|
||
|
|
||
|
VOID
|
||
|
GenerateFilePaths (
|
||
|
VOID
|
||
|
)
|
||
|
{
|
||
|
INT Len;
|
||
|
|
||
|
// Safety (unexpected condition)
|
||
|
if (!g_WorkingDirectory) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Len = CountStringBytes (g_WorkingDirectory) + sizeof(S_SETTINGS_MASK);
|
||
|
g_SettingsFile = (LPSTR) HeapAlloc (g_hHeap, 0, Len);
|
||
|
if (!g_SettingsFile) {
|
||
|
return;
|
||
|
}
|
||
|
wsprintf (g_SettingsFile, S_SETTINGS_MASK, g_WorkingDirectory);
|
||
|
|
||
|
Len = CountStringBytes (g_WorkingDirectory) + sizeof(S_MIGINF_MASK);
|
||
|
g_MigrateDotInf = (LPSTR) HeapAlloc (g_hHeap, 0, Len);
|
||
|
if (!g_MigrateDotInf) {
|
||
|
return;
|
||
|
}
|
||
|
wsprintf (g_MigrateDotInf, S_MIGINF_MASK, g_WorkingDirectory);
|
||
|
}
|
||
|
|
||
|
|
||
|
HKEY
|
||
|
OpenRegKey (
|
||
|
IN HKEY RootKey,
|
||
|
IN LPCSTR KeyStr
|
||
|
)
|
||
|
{
|
||
|
HKEY Key;
|
||
|
LONG rc;
|
||
|
|
||
|
rc = RegOpenKeyEx (RootKey, KeyStr, 0, KEY_ALL_ACCESS, &Key);
|
||
|
if (rc != ERROR_SUCCESS) {
|
||
|
SetLastError (rc);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
pAddKeyToStrMapping (Key, S_HKR, KeyStr);
|
||
|
|
||
|
return Key;
|
||
|
}
|
||
|
|
||
|
|
||
|
HKEY
|
||
|
CreateRegKey (
|
||
|
IN HKEY RootKey,
|
||
|
IN LPCSTR KeyStr
|
||
|
)
|
||
|
{
|
||
|
HKEY Key;
|
||
|
LONG rc;
|
||
|
DWORD DontCare;
|
||
|
|
||
|
pAddKeyToStrMapping (NULL, S_HKR, KeyStr);
|
||
|
|
||
|
rc = RegCreateKeyEx (RootKey, KeyStr, 0, S_EMPTY, 0,
|
||
|
KEY_ALL_ACCESS, NULL, &Key, &DontCare);
|
||
|
if (rc != ERROR_SUCCESS) {
|
||
|
SetLastError (rc);
|
||
|
LogRegistryError (NULL, S_EMPTY);
|
||
|
pRemoveKeyToStrMapping (NULL);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
pRemoveKeyToStrMapping (NULL);
|
||
|
pAddKeyToStrMapping (Key, S_HKR, KeyStr);
|
||
|
|
||
|
return Key;
|
||
|
}
|
||
|
|
||
|
VOID
|
||
|
CloseRegKey (
|
||
|
IN HKEY Key
|
||
|
)
|
||
|
{
|
||
|
pRemoveKeyToStrMapping (Key);
|
||
|
RegCloseKey (Key);
|
||
|
}
|
||
|
|
||
|
|
||
|
LPCSTR
|
||
|
GetRegValueString (
|
||
|
IN HKEY Key,
|
||
|
IN LPCSTR ValueName
|
||
|
)
|
||
|
{
|
||
|
static CHAR DataBuf[MAX_PATH];
|
||
|
DWORD Size;
|
||
|
LONG rc;
|
||
|
DWORD Type;
|
||
|
DWORD d;
|
||
|
|
||
|
Size = MAX_PATH;
|
||
|
rc = RegQueryValueEx (Key, ValueName, NULL, &Type, DataBuf, &Size);
|
||
|
SetLastError (rc);
|
||
|
|
||
|
if (rc != ERROR_SUCCESS) {
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
if (Type == REG_DWORD) {
|
||
|
d = *((PDWORD) DataBuf);
|
||
|
wsprintf (DataBuf, "%u", d);
|
||
|
}
|
||
|
else if (Type != REG_SZ) {
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
return DataBuf;
|
||
|
}
|
||
|
|
||
|
BOOL
|
||
|
SetRegValueString (
|
||
|
HKEY Key,
|
||
|
LPCSTR ValueName,
|
||
|
LPCSTR ValueStr
|
||
|
)
|
||
|
{
|
||
|
LONG rc;
|
||
|
LPCSTR p;
|
||
|
|
||
|
p = _mbschr (ValueStr, 0);
|
||
|
p++;
|
||
|
|
||
|
rc = RegSetValueEx (Key, ValueName, 0, REG_SZ, ValueStr, p - ValueStr);
|
||
|
SetLastError (rc);
|
||
|
|
||
|
if (rc != ERROR_SUCCESS) {
|
||
|
LogRegistryError (Key, ValueName);
|
||
|
}
|
||
|
|
||
|
return rc == ERROR_SUCCESS;
|
||
|
}
|
||
|
|
||
|
|
||
|
LPCSTR
|
||
|
GetScrnSaveExe (
|
||
|
VOID
|
||
|
)
|
||
|
{
|
||
|
CHAR IniFileSetting[MAX_PATH];
|
||
|
|
||
|
GetPrivateProfileString (
|
||
|
S_BOOT,
|
||
|
S_SCRNSAVE_EXE,
|
||
|
S_EMPTY,
|
||
|
IniFileSetting,
|
||
|
MAX_PATH,
|
||
|
S_SYSTEM_INI
|
||
|
);
|
||
|
|
||
|
if (!IniFileSetting[0]) {
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
if (!OurGetLongPathName (IniFileSetting, g_Data)) {
|
||
|
// File does not exist
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
return g_Data[0] ? g_Data : NULL;
|
||
|
}
|
||
|
|
||
|
INT
|
||
|
_mbsbytes (
|
||
|
IN LPCSTR str
|
||
|
)
|
||
|
{
|
||
|
LPCSTR p;
|
||
|
|
||
|
// Find the nul terminator and return the number of bytes
|
||
|
// occupied by the characters in the string, but don't
|
||
|
// include the nul.
|
||
|
|
||
|
p = _mbschr (str, 0);
|
||
|
return (p - str);
|
||
|
}
|
||
|
|
||
|
|
||
|
DWORD
|
||
|
CountStringBytes (
|
||
|
IN LPCSTR str
|
||
|
)
|
||
|
{
|
||
|
// Return bytes in string, plus 1 for the nul
|
||
|
return _mbsbytes (str) + 1;
|
||
|
}
|
||
|
|
||
|
DWORD
|
||
|
CountMultiStringBytes (
|
||
|
IN LPCSTR str
|
||
|
)
|
||
|
{
|
||
|
LPCSTR p;
|
||
|
INT Total = 0;
|
||
|
INT Bytes;
|
||
|
|
||
|
p = str;
|
||
|
|
||
|
do {
|
||
|
Bytes = CountStringBytes (p);
|
||
|
p += Bytes;
|
||
|
Total += Bytes;
|
||
|
} while (Bytes > 1);
|
||
|
|
||
|
return Total;
|
||
|
}
|
||
|
|
||
|
|
||
|
LPSTR
|
||
|
CopyStringAtoB (
|
||
|
OUT LPSTR mbstrDest,
|
||
|
IN LPCSTR mbstrStart,
|
||
|
IN LPCSTR mbstrEnd // first char NOT to copy
|
||
|
)
|
||
|
{
|
||
|
LPSTR mbstrOrg;
|
||
|
|
||
|
mbstrOrg = mbstrDest;
|
||
|
|
||
|
// Assume mbstrEnd is on a lead byte
|
||
|
|
||
|
while (mbstrStart < mbstrEnd) {
|
||
|
if (isleadbyte (*mbstrStart)) {
|
||
|
*mbstrDest = *mbstrStart;
|
||
|
mbstrDest++;
|
||
|
mbstrStart++;
|
||
|
}
|
||
|
|
||
|
*mbstrDest = *mbstrStart;
|
||
|
mbstrDest++;
|
||
|
mbstrStart++;
|
||
|
}
|
||
|
|
||
|
*mbstrDest = 0;
|
||
|
|
||
|
return mbstrOrg;
|
||
|
}
|
||
|
|
||
|
LPSTR
|
||
|
AppendStr (
|
||
|
OUT LPSTR mbstrDest,
|
||
|
IN LPCSTR mbstrSrc
|
||
|
)
|
||
|
|
||
|
{
|
||
|
// Advance mbstrDest to end of string
|
||
|
mbstrDest = _mbschr (mbstrDest, 0);
|
||
|
|
||
|
// Copy string
|
||
|
while (*mbstrSrc) {
|
||
|
*mbstrDest = *mbstrSrc++;
|
||
|
if (isleadbyte (*mbstrDest)) {
|
||
|
mbstrDest++;
|
||
|
*mbstrDest = *mbstrSrc++;
|
||
|
}
|
||
|
mbstrDest++;
|
||
|
}
|
||
|
|
||
|
*mbstrDest = 0;
|
||
|
|
||
|
return mbstrDest;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
pFindShortName (
|
||
|
IN LPCTSTR WhatToFind,
|
||
|
OUT LPTSTR Buffer
|
||
|
)
|
||
|
{
|
||
|
WIN32_FIND_DATA fd;
|
||
|
HANDLE hFind;
|
||
|
|
||
|
hFind = FindFirstFile (WhatToFind, &fd);
|
||
|
if (hFind == INVALID_HANDLE_VALUE) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
FindClose (hFind);
|
||
|
_mbscpy (Buffer, fd.cFileName);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
OurGetLongPathName (
|
||
|
IN LPCSTR ShortPath,
|
||
|
OUT LPSTR Buffer
|
||
|
)
|
||
|
{
|
||
|
CHAR FullPath[MAX_PATH];
|
||
|
LPSTR FilePart;
|
||
|
LPSTR BufferEnd;
|
||
|
LPSTR p, p2;
|
||
|
CHAR c;
|
||
|
|
||
|
//
|
||
|
// Convert ShortPath into complete path name
|
||
|
//
|
||
|
|
||
|
if (!_mbschr (ShortPath, TEXT('\\'))) {
|
||
|
if (!SearchPath (NULL, ShortPath, NULL, MAX_PATH, FullPath, &FilePart)) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
} else {
|
||
|
GetFullPathName (ShortPath, MAX_PATH, FullPath, &FilePart);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Convert short path to long path
|
||
|
//
|
||
|
|
||
|
p = FullPath;
|
||
|
|
||
|
// Don't process non-local paths
|
||
|
if (!(*p) || _mbsnextc (_mbsinc (p)) != TEXT(':')) {
|
||
|
_mbscpy (Buffer, FullPath);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
p = _mbsinc (p);
|
||
|
p = _mbsinc (p);
|
||
|
if (_mbsnextc (p) != TEXT('\\')) {
|
||
|
_mbscpy (Buffer, FullPath);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
// Copy drive letter to buffer
|
||
|
p = _mbsinc (p);
|
||
|
CopyStringAtoB (Buffer, FullPath, p);
|
||
|
BufferEnd = _mbschr (Buffer, 0);
|
||
|
|
||
|
// Convert each portion of the path
|
||
|
do {
|
||
|
// Locate end of this file or dir
|
||
|
p2 = _mbschr (p, TEXT('\\'));
|
||
|
if (!p2) {
|
||
|
p = _mbschr (p, 0);
|
||
|
} else {
|
||
|
p = p2;
|
||
|
}
|
||
|
|
||
|
// Look up file
|
||
|
c = *p;
|
||
|
*p = 0;
|
||
|
if (!pFindShortName (FullPath, BufferEnd)) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
*p = c;
|
||
|
|
||
|
// Move on to next part of path
|
||
|
BufferEnd = _mbschr (BufferEnd, 0);
|
||
|
if (*p) {
|
||
|
p = _mbsinc (p);
|
||
|
BufferEnd = AppendStr (BufferEnd, TEXT("\\"));
|
||
|
}
|
||
|
} while (*p);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL
|
||
|
CreateScreenSaverParamKey (
|
||
|
IN LPCSTR ScreenSaverName,
|
||
|
IN LPCSTR ValueName,
|
||
|
OUT LPSTR Buffer
|
||
|
)
|
||
|
{
|
||
|
//
|
||
|
// Make sure we cannot create a string bigger than MAX_PATH
|
||
|
//
|
||
|
|
||
|
if (_mbslen (ScreenSaverName) + 4 + _mbslen (ValueName) > MAX_PATH) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Format the string with length of screen saver name, screen saver name,
|
||
|
// and value name.
|
||
|
//
|
||
|
|
||
|
wsprintf (Buffer, "%03u/%s/%s", _mbslen (ScreenSaverName), ScreenSaverName, ValueName);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
BOOL
|
||
|
DecodeScreenSaverParamKey (
|
||
|
IN LPCSTR EncodedString,
|
||
|
OUT LPSTR ScreenSaverName,
|
||
|
OUT LPSTR ValueName
|
||
|
)
|
||
|
{
|
||
|
INT Len;
|
||
|
|
||
|
//
|
||
|
// Validate encoded string. It is in the form of ###/screen saver name/value name.
|
||
|
//
|
||
|
|
||
|
Len = atoi (EncodedString);
|
||
|
if (Len < 0 || Len >= MAX_PATH || (Len - 5) > (INT) _mbslen (EncodedString)) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
if (EncodedString[3] != '/' || EncodedString[4 + Len] != '/') {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
if (_mbslen (EncodedString + 5 + Len) >= MAX_PATH) {
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Extract screen saver name and value name
|
||
|
//
|
||
|
|
||
|
_mbsncpy (ScreenSaverName, EncodedString + 4, Len);
|
||
|
ScreenSaverName[Len] = 0;
|
||
|
|
||
|
_mbscpy (ValueName, EncodedString + 5 + Len);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
LPSTR
|
||
|
_mbsistr (
|
||
|
IN LPCSTR mbstrStr,
|
||
|
IN LPCSTR mbstrSubStr
|
||
|
)
|
||
|
{
|
||
|
LPCSTR mbstrStart, mbstrStrPos, mbstrSubStrPos;
|
||
|
LPCSTR mbstrEnd;
|
||
|
|
||
|
mbstrEnd = (LPSTR) ((LPBYTE) mbstrStr + _mbsbytes (mbstrStr) - _mbsbytes (mbstrSubStr));
|
||
|
|
||
|
for (mbstrStart = mbstrStr ; mbstrStart <= mbstrEnd ; mbstrStart = _mbsinc (mbstrStart)) {
|
||
|
mbstrStrPos = mbstrStart;
|
||
|
mbstrSubStrPos = mbstrSubStr;
|
||
|
|
||
|
while (*mbstrSubStrPos &&
|
||
|
_mbctolower ((INT) _mbsnextc (mbstrSubStrPos)) == _mbctolower ((INT) _mbsnextc (mbstrStrPos)))
|
||
|
{
|
||
|
mbstrStrPos = _mbsinc (mbstrStrPos);
|
||
|
mbstrSubStrPos = _mbsinc (mbstrSubStrPos);
|
||
|
}
|
||
|
|
||
|
if (!(*mbstrSubStrPos))
|
||
|
return (LPSTR) mbstrStart;
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
VOID
|
||
|
DeletePartOfString (
|
||
|
IN LPSTR Buffer,
|
||
|
IN DWORD CharsToDelete
|
||
|
)
|
||
|
{
|
||
|
LPSTR p;
|
||
|
DWORD d;
|
||
|
|
||
|
p = Buffer;
|
||
|
for (d = 0 ; *p && d < CharsToDelete ; d++) {
|
||
|
p = _mbsinc (p);
|
||
|
}
|
||
|
|
||
|
if (!(*p)) {
|
||
|
*Buffer = 0;
|
||
|
} else {
|
||
|
MoveMemory (Buffer, p, CountStringBytes(p));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
VOID
|
||
|
InsertStringInString (
|
||
|
IN LPSTR Buffer,
|
||
|
IN LPCSTR StringToInsert
|
||
|
)
|
||
|
{
|
||
|
DWORD BytesToMove;
|
||
|
DWORD BytesOfInsertedString;
|
||
|
|
||
|
BytesToMove = CountStringBytes (Buffer);
|
||
|
BytesOfInsertedString = _mbsbytes(StringToInsert);
|
||
|
MoveMemory (Buffer + BytesOfInsertedString,
|
||
|
Buffer,
|
||
|
BytesToMove
|
||
|
);
|
||
|
_mbsncpy (Buffer, StringToInsert, _mbslen (StringToInsert));
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
PCSTR
|
||
|
ParseMessage (
|
||
|
UINT MessageId,
|
||
|
...
|
||
|
)
|
||
|
{
|
||
|
va_list list;
|
||
|
PSTR RetStr = NULL;
|
||
|
UINT RetStrSize;
|
||
|
|
||
|
va_start (list, MessageId);
|
||
|
|
||
|
RetStrSize = FormatMessageA (
|
||
|
FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_HMODULE,
|
||
|
(PSTR) g_hInst,
|
||
|
MessageId,
|
||
|
0,
|
||
|
(PSTR) (&RetStr),
|
||
|
1,
|
||
|
&list
|
||
|
);
|
||
|
|
||
|
if (!RetStrSize && RetStr) {
|
||
|
*RetStr = 0;
|
||
|
}
|
||
|
|
||
|
va_end (list);
|
||
|
|
||
|
return RetStr;
|
||
|
}
|
||
|
|
||
|
VOID
|
||
|
FreeMessage (
|
||
|
PCSTR Message
|
||
|
)
|
||
|
{
|
||
|
if (Message) {
|
||
|
LocalFree ((PSTR) Message);
|
||
|
}
|
||
|
}
|
||
|
|