657 lines
14 KiB
C++
657 lines
14 KiB
C++
#include <win32.h>
|
|
#include <vfw.h>
|
|
#include "debug.h"
|
|
#include "fileshar.h"
|
|
|
|
#ifdef USE_DIRECTIO
|
|
#include "directio.h"
|
|
#endif
|
|
|
|
#define MAXTASKS 10
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
#define CurrentProcess() ((HANDLE) GetCurrentProcessId())
|
|
#else
|
|
#define CurrentProcess() ((HANDLE) GetCurrentPDB())
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#define HSHfromPSH(psh) (HSHFILE) psh
|
|
#define PSHfromHSH(hsh) (PSHFILE) hsh
|
|
#else
|
|
#define HSHfromPSH(psh) (HSHFILE) GlobalPtrHandle(psh)
|
|
#define PSHfromHSH(hsh) (PSHFILE) GlobalLock((HGLOBAL) hsh)
|
|
#endif
|
|
//
|
|
// allow multiple processes to use the same file handle (as will happen on
|
|
// win16 and chicago when an interface pointer is simply-marshalled to another
|
|
// process that shares the same global address space).
|
|
//
|
|
// This will not happen on NT, but we retain the code structure.
|
|
//
|
|
|
|
|
|
#ifdef USE_DIRECTIO
|
|
|
|
// use unbuffered i/o direct to the disk, rather than going through
|
|
// mmio and the disk buffer. much faster for streaming reads and writes.
|
|
// open via mmio if direct io not possible (eg mmio handler installed).
|
|
|
|
#endif
|
|
|
|
|
|
|
|
typedef struct {
|
|
#ifndef DAYTONA
|
|
TCHAR szFile[256];
|
|
DWORD dwOpenFlags;
|
|
MMIOINFO mmioinfo;
|
|
|
|
HANDLE htask;
|
|
int i;
|
|
LONG lOffset;
|
|
|
|
HANDLE ahtask[MAXTASKS];
|
|
HMMIO ahmmio[MAXTASKS];
|
|
ULONG ulRef[MAXTASKS];
|
|
#else
|
|
ULONG ulRef;
|
|
#endif
|
|
|
|
|
|
HMMIO hmmio;
|
|
|
|
#ifdef USE_DIRECTIO
|
|
CFileStream * pdio;
|
|
#ifndef DAYTONA
|
|
CFileStream * adio[MAXTASKS];
|
|
#endif
|
|
#endif
|
|
|
|
} SHFILE, FAR * PSHFILE;
|
|
|
|
|
|
#ifdef DAYTONA
|
|
#define GetProperTask(psh) (TRUE)
|
|
#else
|
|
|
|
extern "C" {
|
|
extern LPTSTR FAR lstrzcpy (LPTSTR pszTgt, LPCTSTR pszSrc, size_t cch);
|
|
extern LPSTR FAR lstrzcpyA (LPSTR pszTgt, LPCSTR pszSrc, size_t cch);
|
|
extern LPWSTR FAR lstrzcpyW (LPWSTR pszTgt, LPCWSTR pszSrc, size_t cch);
|
|
extern LPWSTR FAR lstrzcpyAtoW (LPWSTR pszTgt, LPCSTR pszSrc, size_t cch);
|
|
extern LPSTR FAR lstrzcpyWtoA (LPSTR pszTgt, LPCWSTR pszSrc, size_t cch);
|
|
} // extern "C"
|
|
|
|
BOOL GetProperTask(PSHFILE psh)
|
|
{
|
|
HANDLE htask = CurrentProcess();
|
|
int i;
|
|
|
|
if (htask == psh->htask)
|
|
return
|
|
#ifdef USE_DIRECTIO
|
|
(psh->pdio != NULL) ||
|
|
#endif
|
|
((psh->hmmio != 0) && (psh->hmmio != (HMMIO) -1));
|
|
|
|
for (i = 0; i < MAXTASKS; i++) {
|
|
if (psh->ahtask[i] == htask) {
|
|
Success:
|
|
psh->hmmio = psh->ahmmio[i];
|
|
psh->htask = htask;
|
|
psh->i = i;
|
|
#ifdef USE_DIRECTIO
|
|
psh->pdio = psh->adio[i];
|
|
if (psh->pdio != NULL) {
|
|
|
|
psh->pdio->Seek(psh->lOffset);
|
|
return TRUE;
|
|
|
|
}
|
|
#endif
|
|
|
|
mmioSeek(psh->hmmio, psh->lOffset, SEEK_SET);
|
|
return (psh->hmmio != 0) && (psh->hmmio != (HMMIO) -1);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < MAXTASKS; i++) {
|
|
if (psh->ahtask[i] == 0) {
|
|
DPF2("Re-opening handle %lx in task %x\n", psh, htask);
|
|
|
|
#ifdef USE_DIRECTIO
|
|
psh->adio[i] = new CFileStream;
|
|
if (!psh->adio[i]->Open(psh->szFile,
|
|
(psh->dwOpenFlags & OF_READWRITE),
|
|
(psh->dwOpenFlags & OF_CREATE))) {
|
|
|
|
delete psh->adio[i];
|
|
psh->adio[i] = NULL;
|
|
#else
|
|
{
|
|
#endif
|
|
psh->ahmmio[i] = mmioOpen(psh->szFile, NULL, psh->dwOpenFlags);
|
|
psh->ahtask[i] = htask;
|
|
if (psh->ahmmio[i] == 0) {
|
|
DPF("mmioOpen failed in GetProperTask!\n");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
goto Success;
|
|
}
|
|
}
|
|
|
|
DPF("File handle open in too many tasks!\n");
|
|
|
|
return FALSE;
|
|
}
|
|
#endif
|
|
|
|
|
|
HSHFILE WINAPI shfileOpen(LPTSTR szFileName, MMIOINFO FAR* lpmmioinfo,
|
|
DWORD dwOpenFlags)
|
|
{
|
|
PSHFILE psh = (PSHFILE) GlobalAllocPtr(GPTR | GMEM_SHARE, sizeof(SHFILE));
|
|
|
|
if (!psh)
|
|
return NULL;
|
|
|
|
#ifndef DAYTONA
|
|
lstrzcpy(psh->szFile, szFileName, NUMELMS(psh->szFile));
|
|
psh->dwOpenFlags = dwOpenFlags;
|
|
#endif
|
|
|
|
|
|
psh->hmmio = NULL;
|
|
#ifdef USE_DIRECTIO
|
|
if (
|
|
// Direct I/O is broken for reading the end of files on Chicago. Don't use it.
|
|
#ifndef DIRECTIOFORREADINGALSO
|
|
!(dwOpenFlags & OF_CREATE) ||
|
|
#endif
|
|
!(psh->pdio = new CFileStream) ||
|
|
!psh->pdio->Open(
|
|
szFileName,
|
|
(dwOpenFlags & (OF_WRITE | OF_READWRITE)),
|
|
(dwOpenFlags & OF_CREATE)))
|
|
{
|
|
if (psh->pdio)
|
|
delete psh->pdio;
|
|
psh->pdio = NULL;
|
|
#else
|
|
{
|
|
#endif
|
|
psh->hmmio = mmioOpen(szFileName, lpmmioinfo, dwOpenFlags);
|
|
|
|
if (psh->hmmio == 0) {
|
|
DPF("mmioOpen failed!\n");
|
|
GlobalFreePtr(psh);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
//DPF("Opening handle %lx ('%s') in task %x, mode = %lx\n", psh, szFileName, CurrentProcess(), psh->dwOpenFlags);
|
|
|
|
|
|
#ifndef DAYTONA
|
|
psh->ahmmio[0] = psh->hmmio;
|
|
psh->ahtask[0] = psh->htask = CurrentProcess();
|
|
psh->ulRef[0] = 1; // !!! 0?
|
|
|
|
#ifdef USE_DIRECTIO
|
|
psh->adio[0] = psh->pdio;
|
|
#endif
|
|
|
|
#else
|
|
psh->ulRef = 1; // !!! 0?
|
|
#endif
|
|
|
|
return HSHfromPSH(psh);
|
|
}
|
|
|
|
UINT WINAPI shfileClose(HSHFILE hsh, UINT uFlags)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
#ifndef DAYTONA
|
|
int i;
|
|
|
|
for (i = 0; i < MAXTASKS; i++) {
|
|
if (psh->ahtask[i] && psh->ahmmio[i]) {
|
|
DPF("Handle %lx closed with ref count %ld in task %x\n", psh, psh->ulRef[i], psh->ahtask[i]);
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->adio[i]) {
|
|
delete psh->adio[i];
|
|
psh->adio[i] = NULL;
|
|
} else
|
|
#endif
|
|
mmioClose(psh->ahmmio[i], 0);
|
|
}
|
|
}
|
|
#else
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
delete psh->pdio;
|
|
psh->pdio = NULL;
|
|
} else
|
|
#endif
|
|
if (psh->hmmio) {
|
|
mmioClose(psh->hmmio, 0);
|
|
}
|
|
#endif
|
|
|
|
|
|
GlobalFreePtr(psh);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef USE_DIRECTIO
|
|
// if we are using direct io, we want to bypass the buffering
|
|
// schemes that are layered on top of this module. Allow them to
|
|
// determine if we are using direct io to do this.
|
|
BOOL shfileIsDirect(HSHFILE hsh)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
return (psh->pdio != 0);
|
|
}
|
|
|
|
void
|
|
shfileStreamStart(HSHFILE hsh)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return;
|
|
|
|
if (psh->pdio == 0) {
|
|
return;
|
|
}
|
|
|
|
psh->pdio->StartStreaming();
|
|
}
|
|
|
|
void shfileStreamStop(HSHFILE hsh)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return;
|
|
|
|
if (psh->pdio == 0) {
|
|
return;
|
|
}
|
|
|
|
psh->pdio->StopStreaming();
|
|
}
|
|
|
|
#endif
|
|
|
|
LONG WINAPI shfileRead(HSHFILE hsh, HPSTR pch, LONG cch)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
#ifndef DAYTONA
|
|
psh->lOffset += cch;
|
|
#endif
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
DWORD bytes;
|
|
|
|
if (!psh->pdio->Read((LPBYTE)pch, cch, &bytes)) {
|
|
return 0;
|
|
} else {
|
|
return bytes;
|
|
}
|
|
} else
|
|
#endif
|
|
return mmioRead(psh->hmmio, pch, cch);
|
|
}
|
|
|
|
LONG WINAPI shfileWrite(HSHFILE hsh, const char _huge* pch, LONG cch)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
#ifndef DAYTONA
|
|
psh->lOffset += cch;
|
|
#endif
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
DWORD bytes;
|
|
|
|
if (!psh->pdio->Write((LPBYTE)pch, cch, &bytes)) {
|
|
return 0;
|
|
} else {
|
|
return bytes;
|
|
}
|
|
} else
|
|
#endif
|
|
return mmioWrite(psh->hmmio, pch, cch);
|
|
}
|
|
|
|
LONG WINAPI shfileSeek(HSHFILE hsh, LONG lOffset, int iOrigin)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
|
|
Assert(iOrigin != SEEK_END);
|
|
|
|
if (iOrigin == SEEK_CUR) {
|
|
lOffset += psh->pdio->GetCurrentPosition();
|
|
}
|
|
|
|
psh->pdio->Seek(lOffset);
|
|
|
|
#ifndef DAYTONA
|
|
psh->lOffset = psh->pdio->GetCurrentPosition();
|
|
return psh->lOffset;
|
|
#else
|
|
return psh->pdio->GetCurrentPosition();
|
|
#endif
|
|
|
|
} else
|
|
#endif
|
|
{
|
|
|
|
#ifdef DAYTONA
|
|
return mmioSeek(psh->hmmio, lOffset, iOrigin);
|
|
#else
|
|
psh->lOffset = mmioSeek(psh->hmmio, lOffset, iOrigin);
|
|
|
|
return psh->lOffset;
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
LONG WINAPI shfileZero(HSHFILE hsh, LONG lBytes)
|
|
{
|
|
LPVOID pmem;
|
|
LONG lToWrite = lBytes;
|
|
|
|
#define ZERO_AT_ONCE 1024
|
|
pmem = GlobalAllocPtr(GPTR, ZERO_AT_ONCE);
|
|
// We write out 1024 bytes at a time, with the odd bytes being written
|
|
// in the last block. This is probably more efficient than writing the
|
|
// "odd" bytes first, then looping for a known number of iterations to
|
|
// write 1024 bytes at a time.
|
|
if (pmem) {
|
|
LONG cbWrite = ZERO_AT_ONCE;
|
|
while (lToWrite > 0) {
|
|
if (lToWrite < cbWrite) {
|
|
cbWrite = lToWrite;
|
|
}
|
|
if (shfileWrite(hsh, (HPSTR) pmem, cbWrite) != cbWrite) {
|
|
|
|
// The file write has failed. This leaves the file in
|
|
// a bad state. It might be worth trying to position
|
|
// the write pointer as though nothing had been written,
|
|
// but this is problematic as there may be a serious
|
|
// problem with the file itself. Simply abort writing...
|
|
lBytes = -1;
|
|
lToWrite = 0;
|
|
break;
|
|
}
|
|
lToWrite -= cbWrite;
|
|
}
|
|
GlobalFreePtr(pmem);
|
|
return lBytes;
|
|
} else {
|
|
DPF("Unable to allocate 1K of zeroed memory!\n");
|
|
shfileSeek(hsh, lBytes, SEEK_SET);
|
|
return lBytes;
|
|
}
|
|
}
|
|
|
|
LONG WINAPI shfileFlush(HSHFILE hsh, UINT uFlags)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
if (!psh->pdio->CommitAndWait()) {
|
|
return MMIOERR_CANNOTWRITE;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
LONG WINAPI shfileAddRef(HSHFILE hsh)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
#ifdef DAYTONA
|
|
psh->ulRef++;
|
|
#else
|
|
++psh->ulRef[psh->i];
|
|
DPF2("Handle %lx in task %x: ref++ == %ld\n", psh, psh->htask, psh->ulRef[psh->i]);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
LONG WINAPI shfileRelease(HSHFILE hsh)
|
|
{
|
|
PSHFILE psh = PSHfromHSH(hsh);
|
|
|
|
if (!GetProperTask(psh))
|
|
return -1;
|
|
|
|
#ifdef DAYTONA
|
|
if (--psh->ulRef <= 0)
|
|
#else
|
|
if (--psh->ulRef[psh->i] <= 0)
|
|
#endif
|
|
{
|
|
|
|
#ifndef DAYTONA
|
|
DPF2("Closing handle %lx in task %x, pos = %lx\n", psh, psh->htask, psh->lOffset);
|
|
#endif
|
|
|
|
#ifdef USE_DIRECTIO
|
|
if (psh->pdio) {
|
|
delete psh->pdio;
|
|
psh->pdio = 0;
|
|
} else
|
|
#endif
|
|
{
|
|
mmioClose(psh->hmmio, 0);
|
|
}
|
|
psh->hmmio = 0;
|
|
|
|
#ifndef DAYTONA
|
|
|
|
psh->ahmmio[psh->i] = 0;
|
|
psh->ahtask[psh->i] = 0;
|
|
psh->ulRef[psh->i] = 0;
|
|
#ifdef USE_DIRECTIO
|
|
psh->adio[psh->i] = 0;
|
|
#endif
|
|
psh->htask = 0;
|
|
#endif
|
|
|
|
|
|
} else {
|
|
#ifndef DAYTONA
|
|
DPF2("Handle %lx in task %x: ref-- == %ld\n", psh, psh->htask, psh->ulRef[psh->i]);
|
|
#endif
|
|
}
|
|
|
|
|
|
return 0;
|
|
}
|
|
|
|
static BYTE bPad;
|
|
|
|
MMRESULT WINAPI
|
|
shfileDescend(HSHFILE hshfile, LPMMCKINFO lpck, const LPMMCKINFO lpckParent, UINT wFlags)
|
|
{
|
|
FOURCC ckidFind; // chunk ID to find (or NULL)
|
|
FOURCC fccTypeFind; // form/list type to find (or NULL)
|
|
|
|
/* figure out what chunk id and form/list type to search for */
|
|
if (wFlags & MMIO_FINDCHUNK)
|
|
ckidFind = lpck->ckid, fccTypeFind = 0;
|
|
else
|
|
if (wFlags & MMIO_FINDRIFF)
|
|
ckidFind = FOURCC_RIFF, fccTypeFind = lpck->fccType;
|
|
else
|
|
if (wFlags & MMIO_FINDLIST)
|
|
ckidFind = FOURCC_LIST, fccTypeFind = lpck->fccType;
|
|
else
|
|
ckidFind = fccTypeFind = 0;
|
|
|
|
lpck->dwFlags = 0L;
|
|
|
|
while (TRUE)
|
|
{
|
|
UINT w;
|
|
|
|
/* read the chunk header */
|
|
if (shfileRead(hshfile, (HPSTR) lpck, 2 * sizeof(DWORD)) !=
|
|
2 * sizeof(DWORD))
|
|
return MMIOERR_CHUNKNOTFOUND;
|
|
|
|
/* store the offset of the data part of the chunk */
|
|
if ((lpck->dwDataOffset = shfileSeek(hshfile, 0L, SEEK_CUR)) == -1)
|
|
return MMIOERR_CANNOTSEEK;
|
|
|
|
/* check for unreasonable chunk size */
|
|
/* see if the chunk is within the parent chunk (if given) */
|
|
if ((lpckParent != NULL) && (( lpck->dwDataOffset - 8L) >=
|
|
(lpckParent->dwDataOffset + lpckParent->cksize)))
|
|
return MMIOERR_CHUNKNOTFOUND;
|
|
|
|
/* if the chunk if a 'RIFF' or 'LIST' chunk, read the
|
|
* form type or list type
|
|
*/
|
|
if ((lpck->ckid == FOURCC_RIFF) || (lpck->ckid == FOURCC_LIST))
|
|
{
|
|
if (shfileRead(hshfile, (HPSTR) &lpck->fccType,
|
|
sizeof(DWORD)) != sizeof(DWORD))
|
|
return MMIOERR_CHUNKNOTFOUND;
|
|
}
|
|
else
|
|
lpck->fccType = 0;
|
|
|
|
/* if this is the chunk we're looking for, stop looking */
|
|
if ( ((ckidFind == 0) || (ckidFind == lpck->ckid)) &&
|
|
((fccTypeFind == 0) || (fccTypeFind == lpck->fccType)) )
|
|
break;
|
|
|
|
/* ascend out of the chunk and try again */
|
|
if ((w = shfileAscend(hshfile, lpck, 0)) != 0)
|
|
return w;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
MMRESULT WINAPI
|
|
shfileAscend(HSHFILE hshfile, LPMMCKINFO lpck, UINT wFlags)
|
|
{
|
|
if (lpck->dwFlags & MMIO_DIRTY)
|
|
{
|
|
/* <lpck> refers to a chunk created by shfileCreateChunk();
|
|
* check that the chunk size that was written when
|
|
* shfileCreateChunk() was called is the real chunk size;
|
|
* if not, fix it
|
|
*/
|
|
DWORD dwOffset; // current offset in file
|
|
DWORD dwActualSize; // actual size of chunk data
|
|
|
|
if ((dwOffset = (DWORD)shfileSeek(hshfile, 0L, SEEK_CUR)) == -1)
|
|
return MMIOERR_CANNOTSEEK;
|
|
if ((LONG)(dwActualSize = dwOffset - lpck->dwDataOffset) < 0)
|
|
return MMIOERR_CANNOTWRITE;
|
|
|
|
if (LOWORD(dwActualSize) & 1)
|
|
{
|
|
/* chunk size is odd -- write a null pad byte */
|
|
if (shfileWrite(hshfile, (HPSTR) &bPad, sizeof(bPad))
|
|
!= sizeof(bPad))
|
|
return MMIOERR_CANNOTWRITE;
|
|
|
|
}
|
|
|
|
if (lpck->cksize == (DWORD)dwActualSize)
|
|
return 0;
|
|
|
|
/* fix the chunk header */
|
|
lpck->cksize = dwActualSize;
|
|
if (shfileSeek(hshfile, lpck->dwDataOffset
|
|
- sizeof(DWORD), SEEK_SET) == -1)
|
|
return MMIOERR_CANNOTSEEK;
|
|
if (shfileWrite(hshfile, (HPSTR) &lpck->cksize,
|
|
sizeof(DWORD)) != sizeof(DWORD))
|
|
return MMIOERR_CANNOTWRITE;
|
|
}
|
|
|
|
/* seek to the end of the chunk, past the null pad byte
|
|
* (which is only there if chunk size is odd)
|
|
*/
|
|
if (shfileSeek(hshfile, lpck->dwDataOffset + lpck->cksize
|
|
+ (lpck->cksize & 1L), SEEK_SET) == -1)
|
|
return MMIOERR_CANNOTSEEK;
|
|
|
|
return 0;
|
|
}
|
|
|
|
MMRESULT WINAPI
|
|
shfileCreateChunk(HSHFILE hshfile, LPMMCKINFO lpck, UINT wFlags)
|
|
{
|
|
int iBytes; // bytes to write
|
|
DWORD dwOffset; // current offset in file
|
|
|
|
/* store the offset of the data part of the chunk */
|
|
if ((dwOffset = (DWORD)shfileSeek(hshfile, 0L, SEEK_CUR)) == -1)
|
|
return MMIOERR_CANNOTSEEK;
|
|
lpck->dwDataOffset = dwOffset + 2 * sizeof(DWORD);
|
|
|
|
/* figure out if a form/list type needs to be written */
|
|
if (wFlags & MMIO_CREATERIFF)
|
|
lpck->ckid = FOURCC_RIFF, iBytes = 3 * sizeof(DWORD);
|
|
else
|
|
if (wFlags & MMIO_CREATELIST)
|
|
lpck->ckid = FOURCC_LIST, iBytes = 3 * sizeof(DWORD);
|
|
else
|
|
iBytes = 2 * sizeof(DWORD);
|
|
|
|
/* write the chunk header */
|
|
if (shfileWrite(hshfile, (HPSTR) lpck, (LONG) iBytes) != (LONG) iBytes)
|
|
return MMIOERR_CANNOTWRITE;
|
|
|
|
lpck->dwFlags = MMIO_DIRTY;
|
|
|
|
return 0;
|
|
}
|