/* gpgcedrv.c - WindowsCE device driver to implement a pipe. Copyright (C) 2010 Free Software Foundation, Inc. This file is part of Assuan. Assuan is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. Assuan is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, see . */ #include #include #include #include #include #define ENABLE_DEBUG #warning Cancel and caller process termination not handled. /* Missing IOCTLs in the current mingw32ce. */ #ifndef IOCTL_PSL_NOTIFY # define FILE_DEVICE_PSL 259 # define IOCTL_PSL_NOTIFY \ CTL_CODE (259, 255, METHOD_NEITHER, FILE_ANY_ACCESS) #endif /*IOCTL_PSL_NOTIFY*/ /* The IOCTL to return the rendezvous id of the handle. The required outbuf parameter is the address of a variable to store the rendezvous ID, which is a LONG value. */ #define GPGCEDEV_IOCTL_GET_RVID \ CTL_CODE (FILE_DEVICE_STREAMS, 2048, METHOD_BUFFERED, FILE_ANY_ACCESS) /* The IOCTL used to create the pipe. The caller sends this IOCTL to the read or the write handle. The required inbuf parameter is address of a variable holding the rendezvous id of the pipe's other end. There is one possible problem with the code: If a pipe is kept in non-rendezvous state until after the rendezvous ids overflow, it is possible that the wrong end will be used. However this is not a realistic scenario. */ #define GPGCEDEV_IOCTL_MAKE_PIPE \ CTL_CODE (FILE_DEVICE_STREAMS, 2049, METHOD_BUFFERED, FILE_ANY_ACCESS) /* The IOCTL used to unblock a blocking thread. The caller sends this IOCTL to the read or the write handle. No parameter is required. The effect is that a reader or writer blocked on the same handle is unblocked (and will return ERROR_BUSY). Note that the operation can be repeated, if so desired. The state of the pipe itself will not be affected in any way. */ #define GPGCEDEV_IOCTL_UNBLOCK \ CTL_CODE (FILE_DEVICE_STREAMS, 2050, METHOD_BUFFERED, FILE_ANY_ACCESS) struct pipeimpl_s { CRITICAL_SECTION critsect; /* Lock for all members. */ int refcnt; char *buffer; size_t buffer_size; size_t buffer_len; /* The valid length of the bufer. */ size_t buffer_pos; /* The actual read position. */ #define PIPE_FLAG_NO_READER 1 #define PIPE_FLAG_NO_WRITER 2 #define PIPE_FLAG_UNBLOCK_READER 4 #define PIPE_FLAG_UNBLOCK_WRITER 8 int flags; HANDLE space_available; /* Set if space is available. */ HANDLE data_available; /* Set if data is available. */ }; typedef struct pipeimpl_s *pipeimpl_t; /* An object to store information pertaining to an open-context. */ struct opnctx_s; typedef struct opnctx_s *opnctx_t; struct opnctx_s { int inuse; /* True if this object has valid data. */ LONG rvid; /* The unique rendezvous identifier. */ DWORD access_code;/* Value from OpenFile. */ DWORD share_mode; /* Value from OpenFile. */ /* The state shared by all users. */ pipeimpl_t pipeimpl; }; /* A malloced table of open-context and the number of allocated slots. */ static opnctx_t opnctx_table; static size_t opnctx_table_size; /* A criticial section object used to protect the OPNCTX_TABLE. */ static CRITICAL_SECTION opnctx_table_cs; /* We don't need a device context thus we use the adress of the critical section object for it. */ #define DEVCTX_VALUE ((DWORD)(&opnctx_table_cs)) /* Constants used for our lock functions. */ #define LOCK_TRY 0 #define LOCK_WAIT 1 static void log_debug (const char *fmt, ...) { #ifndef ENABLE_DEBUG (void)fmt; #else va_list arg_ptr; FILE *fp; fp = fopen ("\\gpgcedev.log", "a+"); if (!fp) return; va_start (arg_ptr, fmt); vfprintf (fp, fmt, arg_ptr); va_end (arg_ptr); fclose (fp); #endif } /* Return a new rendezvous next command id. Command Ids are used to group resources of one command. We will never return an RVID of 0. */ static LONG create_rendezvous_id (void) { static LONG rendezvous_id; LONG rvid; while (!(rvid = InterlockedIncrement (&rendezvous_id))) ; return rvid; } pipeimpl_t pipeimpl_new (void) { pipeimpl_t pimpl; pimpl = malloc (sizeof (*pimpl)); if (!pimpl) return NULL; InitializeCriticalSection (&pimpl->critsect); pimpl->refcnt = 1; pimpl->buffer_size = 512; pimpl->buffer = malloc (pimpl->buffer_size); pimpl->buffer_len = 0; pimpl->buffer_pos = 0; pimpl->flags = 0; pimpl->space_available = CreateEvent (NULL, FALSE, FALSE, NULL); pimpl->data_available = CreateEvent (NULL, FALSE, FALSE, NULL); return pimpl; } /* PIMPL must be locked. It is unlocked at exit. */ void pipeimpl_unref (pipeimpl_t pimpl) { int release = 0; log_debug ("pipeimpl_unref (%p): dereference\n", pimpl); if (--pimpl->refcnt == 0) release = 1; LeaveCriticalSection (&pimpl->critsect); if (! release) return; log_debug ("pipeimpl_unref (%p): release\n", pimpl); DeleteCriticalSection (&pimpl->critsect); if (pimpl->buffer) { free (pimpl->buffer); pimpl->buffer = NULL; pimpl->buffer_size = 0; } if (pimpl->space_available != INVALID_HANDLE_VALUE) { CloseHandle (pimpl->space_available); pimpl->space_available = INVALID_HANDLE_VALUE; } if (pimpl->data_available != INVALID_HANDLE_VALUE) { CloseHandle (pimpl->data_available); pimpl->data_available = INVALID_HANDLE_VALUE; } } /* Return a new opnctx handle and mark it as used. Returns NULL and sets LastError on memory failure etc. opnctx_table_cs must be locked on entry and is locked on exit. */ static opnctx_t allocate_opnctx (void) { opnctx_t opnctx = NULL; int idx; for (idx = 0; idx < opnctx_table_size; idx++) if (! opnctx_table[idx].inuse) break; if (idx == opnctx_table_size) { /* We need to increase the size of the table. The approach we take is straightforward to minimize the risk of bugs. */ opnctx_t newtbl; size_t newsize = opnctx_table_size + 64; newtbl = calloc (newsize, sizeof *newtbl); if (!newtbl) goto leave; memcpy (newtbl, opnctx_table, opnctx_table_size * sizeof (*newtbl)); free (opnctx_table); opnctx_table = newtbl; idx = opnctx_table_size; opnctx_table_size = newsize; } opnctx = &opnctx_table[idx]; opnctx->inuse = 1; opnctx->rvid = 0; opnctx->access_code = 0; opnctx->share_mode = 0; opnctx->pipeimpl = 0; leave: return opnctx; } /* Verify context CTX, returns NULL if not valid and the pointer to the context if valid. opnctx_table_cs must be locked on entry and is locked on exit. */ opnctx_t verify_opnctx (opnctx_t ctx) { int idx = ctx - opnctx_table; if (idx < 0 || idx >= opnctx_table_size) { SetLastError (ERROR_INVALID_HANDLE); return NULL; } if (! opnctx_table[idx].inuse) { SetLastError (ERROR_INVALID_HANDLE); return NULL; } return &opnctx_table[idx]; } /* Verify access CODE for context CTX, returning a reference to the locked pipe implementation. opnctx_table_cs must be locked on entry and is locked on exit. */ pipeimpl_t access_opnctx (opnctx_t ctx, DWORD code) { int idx; EnterCriticalSection (&opnctx_table_cs); idx = ctx - opnctx_table; if (idx < 0 || idx >= opnctx_table_size || ! opnctx_table[idx].inuse) { SetLastError (ERROR_INVALID_HANDLE); LeaveCriticalSection (&opnctx_table_cs); return NULL; } ctx = &opnctx_table[idx]; if (!(ctx->access_code & code)) { SetLastError (ERROR_INVALID_ACCESS); LeaveCriticalSection (&opnctx_table_cs); return NULL; } if (! ctx->pipeimpl) { ctx->pipeimpl = pipeimpl_new (); if (! ctx->pipeimpl) { log_debug (" access_opnctx (ctx=0x%p): error: can't create pipe\n", ctx); LeaveCriticalSection (&opnctx_table_cs); return NULL; } log_debug (" access_opnctx (ctx=0x%p): created pipe 0x%p\n", ctx, ctx->pipeimpl); } EnterCriticalSection (&ctx->pipeimpl->critsect); ctx->pipeimpl->refcnt++; LeaveCriticalSection (&opnctx_table_cs); return ctx->pipeimpl; } static char * wchar_to_utf8 (const wchar_t *string) { int n; size_t length = wcslen (string); char *result; n = WideCharToMultiByte (CP_UTF8, 0, string, length, NULL, 0, NULL, NULL); if (n < 0 || (n+1) <= 0) abort (); result = malloc (n+1); if (!result) abort (); n = WideCharToMultiByte (CP_ACP, 0, string, length, result, n, NULL, NULL); if (n < 0) abort (); result[n] = 0; return result; } /* Initialize the device and return a device specific context. */ DWORD GPG_Init (LPCTSTR active_key, DWORD bus_context) { char *tmpbuf; (void)bus_context; tmpbuf = wchar_to_utf8 (active_key); log_debug ("GPG_Init (devctx=0x%p, %s)\n", DEVCTX_VALUE, tmpbuf); free (tmpbuf); /* We don't need any global data. However, we need to return something. */ return DEVCTX_VALUE; } /* Deinitialize this device driver. */ BOOL GPG_Deinit (DWORD devctx) { log_debug ("GPG_Deinit (devctx=0x%p)\n", (void*)devctx); if (devctx != DEVCTX_VALUE) { SetLastError (ERROR_INVALID_PARAMETER); return FALSE; /* Error. */ } /* FIXME: Release resources. */ return TRUE; /* Success. */ } /* Create a new open context. This fucntion is called due to a CreateFile from the application. */ DWORD GPG_Open (DWORD devctx, DWORD access_code, DWORD share_mode) { opnctx_t opnctx; log_debug ("GPG_Open (devctx=%p)\n", (void*)devctx); if (devctx != DEVCTX_VALUE) { log_debug ("GPG_Open (devctx=%p): error: devctx mismatch (expected 0x%p)\n", (void*) devctx); SetLastError (ERROR_INVALID_PARAMETER); return 0; /* Error. */ } EnterCriticalSection (&opnctx_table_cs); opnctx = allocate_opnctx (); if (!opnctx) { log_debug ("GPG_Open (devctx=%p): error: could not allocate context\n", (void*) devctx); goto leave; } opnctx->access_code = access_code; opnctx->share_mode = share_mode; log_debug ("GPG_Open (devctx=%p): success: created context 0x%p\n", (void*) devctx, opnctx); leave: LeaveCriticalSection (&opnctx_table_cs); return (DWORD)opnctx; } BOOL GPG_Close (DWORD opnctx_arg) { opnctx_t opnctx = (opnctx_t)opnctx_arg; BOOL result = FALSE; log_debug ("GPG_Close (ctx=0x%p)\n", (void*)opnctx); EnterCriticalSection (&opnctx_table_cs); opnctx = verify_opnctx (opnctx); if (!opnctx) { log_debug ("GPG_Close (ctx=0x%p): could not find context\n", (void*)opnctx); goto leave; } if (opnctx->pipeimpl) { pipeimpl_t pimpl = opnctx->pipeimpl; EnterCriticalSection (&pimpl->critsect); /* This needs to be adjusted if there can be multiple reader/writers. */ if (opnctx->access_code & GENERIC_READ) { pimpl->flags |= PIPE_FLAG_NO_READER; SetEvent (pimpl->space_available); } else if (opnctx->access_code & GENERIC_WRITE) { pimpl->flags |= PIPE_FLAG_NO_WRITER; SetEvent (pimpl->data_available); } pipeimpl_unref (pimpl); opnctx->pipeimpl = 0; } opnctx->access_code = 0; opnctx->share_mode = 0; opnctx->rvid = 0; opnctx->inuse = 0; result = TRUE; log_debug ("GPG_Close (ctx=0x%p): success\n", (void*)opnctx); leave: LeaveCriticalSection (&opnctx_table_cs); return result; } DWORD GPG_Read (DWORD opnctx_arg, void *buffer, DWORD count) { opnctx_t ctx = (opnctx_t)opnctx_arg; pipeimpl_t pimpl; const char *src; char *dst; int result = -1; log_debug ("GPG_Read (ctx=0x%p, buffer=0x%p, count=%d)\n", (void*)ctx, buffer, count); pimpl = access_opnctx (ctx, GENERIC_READ); if (!pimpl) { log_debug ("GPG_Read (ctx=0x%p): error: could not access context\n", (void*)ctx); return -1; } retry: if (pimpl->buffer_pos == pimpl->buffer_len) { HANDLE data_available = pimpl->data_available; /* Check for end of file. */ if (pimpl->flags & PIPE_FLAG_NO_WRITER) { log_debug ("GPG_Read (ctx=0x%p): success: EOF\n", (void*)ctx); result = 0; goto leave; } /* Check for request to unblock once. */ if (pimpl->flags & PIPE_FLAG_UNBLOCK_READER) { log_debug ("GPG_Read (ctx=0x%p): success: EBUSY (due to unblock request)\n", (void*)ctx); pimpl->flags &= ~PIPE_FLAG_UNBLOCK_READER; SetLastError (ERROR_BUSY); result = -1; goto leave; } LeaveCriticalSection (&pimpl->critsect); log_debug ("GPG_Read (ctx=0x%p): waiting: data_available\n", (void*)ctx); WaitForSingleObject (data_available, INFINITE); log_debug ("GPG_Read (ctx=0x%p): resuming: data_available\n", (void*)ctx); EnterCriticalSection (&pimpl->critsect); goto retry; } dst = buffer; src = pimpl->buffer + pimpl->buffer_pos; while (count > 0 && pimpl->buffer_pos < pimpl->buffer_len) { *dst++ = *src++; count--; pimpl->buffer_pos++; } result = (dst - (char*)buffer); if (pimpl->buffer_pos == pimpl->buffer_len) pimpl->buffer_pos = pimpl->buffer_len = 0; /* Now there should be some space available. Signal the write end. Even if COUNT was passed as NULL and no space is available, signaling must be done. */ if (!SetEvent (pimpl->space_available)) log_debug ("GPG_Read (ctx=0x%p): warning: SetEvent(space_available) failed: rc=%d\n", (void*) ctx, (int)GetLastError ()); log_debug ("GPG_Read (ctx=0x%p): success: result=%d\n", (void*)ctx, result); leave: pipeimpl_unref (pimpl); return result; } DWORD GPG_Write (DWORD opnctx_arg, const void *buffer, DWORD count) { opnctx_t ctx = (opnctx_t)opnctx_arg; pipeimpl_t pimpl; int result = -1; const char *src; char *dst; size_t nwritten = 0; log_debug ("GPG_Write (ctx=0x%p, buffer=0x%p, count=%d)\n", (void*)ctx, buffer, count); pimpl = access_opnctx (ctx, GENERIC_WRITE); if (!pimpl) { log_debug ("GPG_Write (ctx=0x%p): error: could not access context\n", (void*)ctx); return -1; } if (!count) { log_debug ("GPG_Write (ctx=0x%p): success\n", (void*)ctx); result = 0; goto leave; } retry: /* Check for broken pipe. */ if (pimpl->flags & PIPE_FLAG_NO_READER) { log_debug ("GPG_Write (ctx=0x%p): error: broken pipe\n", (void*)ctx); SetLastError (ERROR_BROKEN_PIPE); goto leave; } /* Check for request to unblock once. */ if (pimpl->flags & PIPE_FLAG_UNBLOCK_WRITER) { log_debug ("GPG_Write (ctx=0x%p): success: EBUSY (due to unblock request)\n", (void*)ctx); pimpl->flags &= ~PIPE_FLAG_UNBLOCK_WRITER; SetLastError (ERROR_BUSY); result = -1; goto leave; } /* Write to our buffer. */ if (pimpl->buffer_len == pimpl->buffer_size) { /* Buffer is full. */ HANDLE space_available = pimpl->space_available; LeaveCriticalSection (&pimpl->critsect); log_debug ("GPG_Write (ctx=0x%p): waiting: space_available\n", (void*)ctx); WaitForSingleObject (space_available, INFINITE); log_debug ("GPG_Write (ctx=0x%p): resuming: space_available\n", (void*)ctx); EnterCriticalSection (&pimpl->critsect); goto retry; } src = buffer; dst = pimpl->buffer + pimpl->buffer_len; while (count > 0 && pimpl->buffer_len < pimpl->buffer_size) { *dst++ = *src++; count--; pimpl->buffer_len++; nwritten++; } result = nwritten; if (!SetEvent (pimpl->data_available)) log_debug ("GPG_Write (ctx=0x%p): warning: SetEvent(data_available) failed: rc=%d\n", (void*) ctx, (int)GetLastError ()); log_debug ("GPG_Write (ctx=0x%p): success: result=%d\n", (void*)ctx, result); leave: pipeimpl_unref (pimpl); return result; } DWORD GPG_Seek (DWORD opnctx, long amount, WORD type) { SetLastError (ERROR_SEEK_ON_DEVICE); return -1; /* Error. */ } /* opnctx_table_s is locked on entering and on exit. */ static BOOL make_pipe (opnctx_t ctx, LONG rvid) { opnctx_t peerctx = NULL; int idx; log_debug (" make_pipe (ctx=0x%p, rvid=%08lx)\n", ctx, rvid); if (ctx->pipeimpl) { log_debug (" make_pipe (ctx=0x%p): error: already assigned\n", ctx); SetLastError (ERROR_ALREADY_ASSIGNED); return FALSE; } for (idx = 0; idx < opnctx_table_size; idx++) if (opnctx_table[idx].inuse && opnctx_table[idx].rvid == rvid) { peerctx = &opnctx_table[idx]; break; } if (! peerctx) { log_debug (" make_pipe (ctx=0x%p): error: not found\n", ctx); SetLastError (ERROR_NOT_FOUND); return FALSE; } if (peerctx == ctx) { log_debug (" make_pipe (ctx=0x%p): error: target and source identical\n", ctx); SetLastError (ERROR_INVALID_TARGET_HANDLE); return FALSE; } if ((ctx->access_code & GENERIC_READ)) { /* Check that the peer is a write end. */ if (!(peerctx->access_code & GENERIC_WRITE)) { SetLastError (ERROR_INVALID_ACCESS); log_debug (" make_pipe (ctx=0x%p): error: peer is not writer\n", ctx); return FALSE; } } else if ((ctx->access_code & GENERIC_WRITE)) { /* Check that the peer is a read end. */ if (!(peerctx->access_code & GENERIC_READ)) { SetLastError (ERROR_INVALID_ACCESS); log_debug (" make_pipe (ctx=0x%p): error: peer is not reader\n", ctx); return FALSE; } } else { SetLastError (ERROR_INVALID_ACCESS); log_debug (" make_pipe (ctx=0x%p): error: invalid access\n", ctx); return FALSE; } /* Make sure the peer has a pipe implementation to be shared. If not yet, create one. */ if (! peerctx->pipeimpl) { peerctx->pipeimpl = pipeimpl_new (); if (! peerctx->pipeimpl) { log_debug (" make_pipe (ctx=0x%p): error: can't create pipe\n", ctx); return FALSE; } log_debug (" make_pipe (ctx=0x%p): created pipe 0x%p\n", ctx, peerctx->pipeimpl); } EnterCriticalSection (&peerctx->pipeimpl->critsect); peerctx->pipeimpl->refcnt++; ctx->pipeimpl = peerctx->pipeimpl; LeaveCriticalSection (&peerctx->pipeimpl->critsect); log_debug (" make_pipe (ctx=0x%p): success: combined with peer ctx=0x%p (pipe 0x%p)\n", ctx, peerctx, peerctx->pipeimpl); return TRUE; } /* opnctx_table_s is locked on entering and on exit. */ static BOOL unblock_call (opnctx_t ctx) { /* If there is no pipe object, no thread can be blocked. */ if (!ctx->pipeimpl) return TRUE; EnterCriticalSection (&ctx->pipeimpl->critsect); if (ctx->access_code & GENERIC_READ) { ctx->pipeimpl->flags |= PIPE_FLAG_UNBLOCK_READER; SetEvent (ctx->pipeimpl->data_available); } else if (ctx->access_code & GENERIC_WRITE) { ctx->pipeimpl->flags |= PIPE_FLAG_UNBLOCK_WRITER; SetEvent (ctx->pipeimpl->space_available); } LeaveCriticalSection (&ctx->pipeimpl->critsect); return TRUE; } BOOL GPG_IOControl (DWORD opnctx_arg, DWORD code, void *inbuf, DWORD inbuflen, void *outbuf, DWORD outbuflen, DWORD *actualoutlen) { opnctx_t opnctx = (opnctx_t)opnctx_arg; BOOL result = FALSE; LONG rvid; log_debug ("GPG_IOControl (ctx=0x%p, %08x)\n", (void*)opnctx, code); EnterCriticalSection (&opnctx_table_cs); opnctx = verify_opnctx (opnctx); if (!opnctx) { log_debug ("GPG_IOControl (ctx=0x%p): error: could not access context\n", (void*)opnctx); goto leave; } switch (code) { case GPGCEDEV_IOCTL_GET_RVID: log_debug ("GPG_IOControl (ctx=0x%p): code: GET_RVID\n", (void*)opnctx); if (inbuf || inbuflen || !outbuf || outbuflen < sizeof (LONG)) { log_debug ("GPG_IOControl (ctx=0x%p): error: invalid parameter\n", (void*)opnctx); SetLastError (ERROR_INVALID_PARAMETER); goto leave; } if (! opnctx->rvid) opnctx->rvid = create_rendezvous_id (); log_debug ("GPG_IOControl (ctx=0x%p): returning rvid: %08lx\n", (void*)opnctx, opnctx->rvid); memcpy (outbuf, &opnctx->rvid, sizeof (LONG)); if (actualoutlen) *actualoutlen = sizeof (LONG); result = TRUE; break; case GPGCEDEV_IOCTL_MAKE_PIPE: log_debug ("GPG_IOControl (ctx=0x%p): code: MAKE_PIPE\n", (void*)opnctx); if (!inbuf || inbuflen < sizeof (LONG) || outbuf || outbuflen || actualoutlen) { log_debug ("GPG_IOControl (ctx=0x%p): error: invalid parameter\n", (void*)opnctx); SetLastError (ERROR_INVALID_PARAMETER); goto leave; } memcpy (&rvid, inbuf, sizeof (LONG)); log_debug ("GPG_IOControl (ctx=0x%p): requesting to finish pipe for rvid: %08lx\n", (void*)opnctx, rvid); if (make_pipe (opnctx, rvid)) result = TRUE; break; case GPGCEDEV_IOCTL_UNBLOCK: log_debug ("GPG_IOControl (ctx=0x%p): code: UNBLOCK\n", (void*)opnctx); if (inbuf || inbuflen || outbuf || outbuflen || actualoutlen) { log_debug ("GPG_IOControl (ctx=0x%p): error: invalid parameter\n", (void*)opnctx); SetLastError (ERROR_INVALID_PARAMETER); goto leave; } if (unblock_call (opnctx)) result = TRUE; break; case IOCTL_PSL_NOTIFY: log_debug ("GPG_IOControl (ctx=0x%p): code: NOTIFY\n", (void*)opnctx); /* Unexpected process termination. */ break; default: log_debug ("GPG_IOControl (ctx=0x%p): code: (unknown)\n", (void*)opnctx); SetLastError (ERROR_INVALID_PARAMETER); break; } log_debug ("GPG_IOControl (ctx=0x%p): success: result=%d\n", (void*)opnctx, result); leave: LeaveCriticalSection (&opnctx_table_cs); return result; } void GPG_PowerUp (DWORD devctx) { } void GPG_PowerDown (DWORD devctx) { } /* Entry point called by the DLL loader. */ int WINAPI DllMain (HINSTANCE hinst, DWORD reason, LPVOID reserved) { (void)reserved; switch (reason) { case DLL_PROCESS_ATTACH: InitializeCriticalSection (&opnctx_table_cs); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: DeleteCriticalSection (&opnctx_table_cs); break; default: break; } return TRUE; }