Programming of PC/SC Smart Card Interface under Linux

PC/SC (Personal Computer/Smart Card) specification, PC/SC specification as a standard interface between card reader and card and computer, realizes the interaction between card and card reader of different manufacturers.

The source code pcsc-lite running under Linux is http://pcsclite.alioth.debian.org/
There are demo routines

1. Establishing Resource Manager Context

LONG SCardEstablishContext(DWORD dwScope, LPCVOID pvReserved1,LPCVOID pvReserved2, LPSCARDCONTEXT phContext);

Note: This must be the first SCard function called in PC/SC applications, and all applications must establish their own context.
Parameters:
    dwScope: Input Type: Represents the scope of context establishment, establishes local or remote connections, and currently supports SCARD_SCOPE_SYSTEM (completing device database operations in the system domain)
    pvReserved1: Input Type: Reserved, NULL
    pvReserved2: Input Type: Reserved, NULL
    phContext: Output type: Returns the created resource manager context
 Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

//@code
SCARDCONTEXT hContext;
LONG rv;

rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &posContext);
if(rv != SCARD_S_SUCCESS)
{
    LOGE("SCardEstablishContext::failed!\n");
    ret = ERROR_APP_CONTEXT;
    return ret;
}

2. Get a list of card readers available in the system

LONG SCardListReaders(SCARDCONTEXT hContext, /*@unused@*/ LPCSTR mszGroups,LPSTR mszReaders, LPDWORD pcchReaders);
Explain:
    After creating the context, you can get a list of available card readers for system installation in the context

Parameters:
    hContext: Input type; ScardEstablishContext() establishes a resource manager context that cannot be NULL
    mszGroups: Input type; the name of the card reader group, when NULL, means listing all card readers.
    mszReaders: Output type; the name of the card reader installed in the system, separated by' 0'and followed by two consecutive' 0' names
    pcchReaders: Input and Output Types; Length of mszReaders

    This function is used in a variety of ways, mainly to create space for the card reader list in different ways.
    1. When mszReaders are NULL, calling SCardListReaders once gives pcchReaders the size of space they need to output mszReaders, and then applies for memory space by malloc.
    2. When mszReaders are NULL and pcchReaders = SCARD_AUTOALLOCATE, SCardListReaders will automatically apply for memory, but they must be released using SCardFreeMemory(hContext, mszReaders) after use.
    3. When mszReaders are not NULL, they need to be passed in their length with pcchReaders, and SCardListReaders will place the list of card readers in the applied space memory.

Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

Here are three ways to create code examples:

//@code1
SCARDCONTEXT hContext;
LPSTR mszReaders;
DWORD dwReaders;
LONG rv;
...
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
rv = SCardListReaders(hContext, NULL, NULL, &dwReaders);
mszReaders = malloc(sizeof(char)*dwReaders);
rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders);
...
free(mszReaders);

//@code2
SCARDCONTEXT hContext;
LPSTR mszReaders;
DWORD dwReaders;
LONG rv;
...
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
dwReaders = SCARD_AUTOALLOCATE;
rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders);
...
rv = SCardFreeMemory(hContext, mszReaders);

//@code3
SCARDCONTEXT hContext;
LPSTR mszReaders[512];
DWORD dwReaders;
LONG rv;
...
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
dwReaders = sizeof(mszReaders);
rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders);
...

Below is the pick-up code for the name of the card reader.
char name[128][3];
ptr = mszReaders;
int count = 0;
while ((*ptr != '\0') && (dwReaders > 0) && (count < 3))
{
    strcpy(&name[0][count], ptr);
    lens = strlen(&name[0][count]);
    ptr += (lens + 1);
    dwReaders -= (lens + 1);
    count++;
    LOGD("SCardListReaders::Reader Count %d Name:%s\n", count, &name[0][count]);
}
01-17 10:56:44.727: D/smartcardjni(2393): SCardListReaders::Reader Count 1 Name:ZNG Terminal2012-1402 00 00
01-17 10:56:44.727: D/smartcardjni(2393): SCardListReaders::Reader Count 2 Name:ZNG Terminal2012-1402 01 00

3. Insertion of monitoring cards

LONG SCardGetStatusChange(SCARDCONTEXT hContext, DWORD dwTimeout,SCARD_READERSTATE *rgReaderStates, DWORD cReaders);

Explain:
    Blocking monitor card insertion or status change
 Parameters:
    hContext: Input type; Resource Manager context established by ScardEstablishContext()
    dwTimeout: Input type; listen for wait time, 0 or INFINITE (0xFFFFFFFFFFFF) indicates permanent wait
    rgReaderStates: Input and output types; SCARD_READERSTATE structure
    cReaders: Input Type; Card Channel Number
 Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h
typedef struct
{
    const char *szReader; // card reader name
    void *pvUserData; // Private Data
    DWORD dwCurrent State; // Current State of Card Reader
    Events after DWORD dwEventState; //Reader status changes
    DWORD cbAtr; //ATR length
    unsigned char rgbAtr[MAX_ATR_SIZE];//ATR
}
SCARD_READERSTATE, *LPSCARD_READERSTATE;

//@code
SCARDCONTEXT hContext;
SCARD_READERSTATE rgReaderStates[2];
LONG rv;
...
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
...
rgReaderStates[0].szReader = "Reader X";
rgReaderStates[0].dwCurrentState = SCARD_STATE_UNAWARE;

rgReaderStates[1].szReader = "\\\\?PnP?\\Notification";
rgReaderStates[1].dwCurrentState = SCARD_STATE_UNAWARE;
...
rv = SCardGetStatusChange(hContext, INFINITE, rgReaderStates, 2);
printf("reader state: 0x%04X\n", rgReaderStates[0].dwEventState);
printf("reader state: 0x%04X\n", rgReaderStates[1].dwEventState);
If (rgReaderStates [0]. dwEventState & SCARD_STATE_PRESENT)// Card Insertion
{

}
If (rgReaderStates [0]. dwEventState & SCARD_STATE_EMPTY)// Card Removal
{

}
//@endcode

4. Connect Card Reader

LONG SCardConnect(SCARDCONTEXT hContext, LPCSTR szReader,DWORD dwShareMode, DWORD dwPreferredProtocols, LPSCARDHANDLE phCard,LPDWORD pdwActiveProtocol);
Description: After using this interface, the reader will power on and read ATR.

Parameters:
    hContext: Input type; Resource Manager context established by ScardEstablishContext()
    szReader: Input Type: Reader Name to Connect
    dwShareMode: Input type: Connection type SCARD_SHARE_SHARED Multiple applications share the same smart card
                                                SCARD_SHARE_EXCLUSIVE Application Exclusive Smart Card
                                                SCARD_SHARE_DIRECT Private Type, No Access to Other Applications
    DwPreferred Protocols: Input Type: Protocol Type Used 
                                        SCARD_PROTOCOL_T0 T=0 Protocol
                                        SCARD_PROTOCOL_T1 T=1 Protocol
                                        SCARD_PROTOCOL_RAW Original Protocol
    phCard: Output Type: Connection Handle of Card
    pdwActiveProtocol: Output Type: Actual Protocol
                                        SCARD_PROTOCOL_T0   Use the T=0 protocol.
                                        SCARD_PROTOCOL_T1   Use the T=1 protocol.
Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

    rv = SCardConnect(hContext, "Reader X", SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &hCard, &dwActiveProtocol);

5. Obtain card status and ATR

LONG SCardStatus(SCARDHANDLE hCard, LPSTR mszReaderName,LPDWORD pcchReaderLen, LPDWORD pdwState,LPDWORD pdwProtocol, LPBYTE pbAtr, LPDWORD pcbAtrLen);
Explain:
Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()
    mszReaderName: Input and Output Type: Connected Reader Name
    pcchReaderLen: Input and output type: length of mszReaderName, using the same method as pcchReaders in SCardListReaders
    pdwState: Output Type: Current Status of Card Reader
                                SCARD_UNKNOWN           0x0001  /**< Unknown state */
                                SCARD_ABSENT            0x0002  /**< Card is absent */
                                SCARD_PRESENT           0x0004  /**< Card is present */
                                SCARD_SWALLOWED         0x0008  /**< Card not powered */
                                SCARD_POWERED           0x0010  /**< Card is powered */
                                SCARD_NEGOTIABLE        0x0020  /**< Ready for PTS */
                                SCARD_SPECIFIC          0x0040  /**< PTS has been set */
    pdwProtocol: Output Type: Current Protocol Type of Card Reader
                                SCARD_PROTOCOL_T0   Use the T=0 protocol.
                                SCARD_PROTOCOL_T1   Use the T=1 protocol.
    pbAtr: Output Type: ATR of Current Card
    pcbAtrLen: Input and output type: length of pbAtr, using the same method as pcchReaders in SCardListReaders
 Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h
//@code
...
dwAtrLen = sizeof(pbAtr);
dwReaderLen = sizeof(pbReader);
rv = SCardStatus(posReaderList.readerList[using_reader_id].hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv != SCARD_S_SUCCESS)
{
    LOGE("SCardStatus %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
    ret = SCARD_STATUS_FALSE;
}
LOGD("pbReader: %s\n", pbReader);
LOGD("pbAtr: %s\n", pbAtr);

6. Data transmission of card reader

LONG SCardTransmit(SCARDHANDLE hCard, const SCARD_IO_REQUEST *pioSendPci,LPCBYTE pbSendBuffer, DWORD cbSendLength, SCARD_IO_REQUEST *pioRecvPci, LPBYTE pbRecvBuffer,LPDWORD pcbRecvLength);

Description: APDU data transmission can be carried out after successful connection of slave card
 Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()
    pioSendPci: Input and Output Type: A Pointer to the Header Structure of the Sending Instruction Protocol, defined by the SCARD_IO_REQUEST Structure
    pbSendBuffer: Input type: APPU instruction sent to card
    cbSendLength: Input type: APDU length
    pioRecvPci: Input and Output Type: A pointer to the received instruction protocol header structure, defined by the SCARD_IO_REQUEST structure and set to NULL if not returned
    pbRecvBuffer: Output Type: Data Returned from Card
    pcbRecvLength: Input and Output Type: Actual Size of pbRecvBuffer
 Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h


typedef struct
{
    unsigned long dwProtocol;   /**< Protocol identifier */
    unsigned long cbPciLength;  /**< Protocol Control Inf Length */
}SCARD_IO_REQUEST, *PSCARD_IO_REQUEST, *LPSCARD_IO_REQUEST;

//@code
rv = SCardConnect(hContext, "Reader X", SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0, &hCard, &dwActiveProtocol);
dwSendLength = sizeof(pbSendBuffer);
dwRecvLength = sizeof(pbRecvBuffer);
rv = SCardTransmit(hCard, SCARD_PCI_T0, pbSendBuffer, dwSendLength,&pioRecvPci, pbRecvBuffer, &dwRecvLength);

7. Exclusive visits

This set of operations is used before and after SCardTransmit(), making the transmission operation safer and more reliable.
LONG SCardBeginTransaction(SCARDHANDLE hCard);
Description: Start a transaction to prevent other applications from accessing smart cards
 Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()


LONG SCardEndTransaction(SCARDHANDLE hCard, DWORD dwDisposition);
Description: End a transaction and allow other reference programs to access smart cards
 Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()
    dwDisposition input type: end of operation on card reader

8. Reconnect the card reader

LONG SCardReconnect(SCARDHANDLE hCard, DWORD dwShareMode,DWORD dwPreferredProtocols, DWORD dwInitialization, LPDWORD pdwActiveProtocol);
Description: Connect the card reader that is being connected again, equal to hot reset
 Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()
    dwShareMode: Input type: Connection type SCARD_SHARE_SHARED Multiple applications share the same smart card
                                        SCARD_SHARE_EXCLUSIVE Application Exclusive Smart Card
                                        SCARD_SHARE_DIRECT Private Type, No Access to Other Applications
    DwPreferred Protocols: Input Type: Protocol Type Used 
                                        SCARD_PROTOCOL_T0 T=0 Protocol
                                        SCARD_PROTOCOL_T1 T=1 Protocol
                                        SCARD_PROTOCOL_RAW Original Protocol
    dwInitialization: Input type: The operation specified by the card reader, often used for restart
                                        SCARD_LEAVE_CARD     - Do nothing.
                                        SCARD_RESET_CARD     - Reset the card (warm reset).
                                        SCARD_UNPOWER_CARD   - Power down the card (cold reset).
                                        SCARD_EJECT_CARD     - Eject the card.
    pdwActiveProtocol: Output Type: Actual Protocol

Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

9. Disconnect the card reader

LONG SCardDisconnect(SCARDHANDLE hCard, DWORD dwDisposition);
Description: disconnect card reader connection
 Parameters:
    hCard: Input type: Card connection handle established by SCardConnect()
    dwDisposition input type: operation of card reader when disconnected
                                SCARD_LEAVE_CARD        0x0000  /**< Do nothing on close */
                                SCARD_RESET_CARD        0x0001  /**< Reset on close */
                                SCARD_UNPOWER_CARD      0x0002  /**< Power down on close */
                                SCARD_EJECT_CARD        0x0003  /**< Eject on close */
Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

//@code
rv = SCardDisconnect(hCard, SCARD_UNPOWER_CARD);

10. Release context

LONG SCardReleaseContext(SCARDCONTEXT hContext);
Description: Release resource manager context before application termination
 Parameters:
    hContext: Input type; Resource Manager context established by ScardEstablishContext()
Return:
    Successful return to SCARD_S_SUCCESS
    Failure returns other values, defined in pcsclite.h

Sample code:

   //***********************************
   #define MAX_READER_COUNTS    3

   typedef struct _strReaderListInfo
   {
    char reader[128];
    int trans_begin;
    SCARDHANDLE hCard;
    DWORD dwActiveProtocol;
    SCARD_IO_REQUEST pioSendPci;
    SCARD_IO_REQUEST pioRecvPci;
   }StrReaderListInfo, *pStrReaderListInfo;

   typedef struct _strReaderList
   {
    int count;
    StrReaderListInfo readerList[MAX_READER_COUNTS];
   }StrReaderList, *pStrReaderList;

   unsigned int posReaderId = 1;    //readerid
   StrReaderList posReaderList = {0};
   SCARDCONTEXT posContext = 0;
   DWORD posActiveProtocol[MAX_READER_COUNTS];



   //Context initialization
   unsigned int Pcscd_Init()
   {
    LOGD("%s",__FUNCTION__);
    unsigned int ret = ERROR_NONE;
    LONG rv = 0;
    unsigned int lens = 0;
    DWORD dwReaders;
    unsigned char reader_list_buf[512];
    pStrReaderListInfo pStrInfo;
    unsigned char *ptr = NULL;
    unsigned int tryAgain = 0;

   START:
    if(posContext != 0)
    {
        LOGE("SCardEstablishContext::Context has already been created,Release!\n");
        rv = SCardReleaseContext(hContext);
        if(rv != SCARD_S_SUCCESS)
        {
            LOGE("SCardReleaseContext::failed!\n");
        }
        hContext = 0;
    }
    //Create context
    memset(&posReaderList, 0, sizeof(StrReaderList));
    rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &posContext);
    if(rv != SCARD_S_SUCCESS)
    {
        LOGE("SCardEstablishContext::failed!\n");
        ret = ERROR_APP_CONTEXT;
        return ret;
    }
    LOGD("SCardEstablishContext::success!\n");

    //Get the card reader list
    dwReaders = sizeof(reader_list_buf);
    rv = SCardListReaders(posContext, NULL, reader_list_buf, &dwReaders);               
    if(rv != SCARD_S_SUCCESS)
    {
        LOGE("SCardListReaders::native failed!(0x%x)\n",rv);
        if(posContext != 0)
        {
            SCardReleaseContext(posContext);
            posContext = 0;

        }
        if(rv == SCARD_E_NO_READERS_AVAILABLE)
        {
            ret = ERROR_APP_REBOOT_POS;
        }
        else
        {
            ret = ERROR_APP_LISTREADER;
        }
        return ret;
    }
    reader_list_buf[dwReaders] = 0x00;
    reader_list_buf[dwReaders + 1] = 0x00;

    posReaderList.count = 0;
    ptr = reader_list_buf;
    while ((*ptr != '\0') && (dwReaders > 0))
    {
        pStrInfo = &posReaderList.readerList[posReaderList.count];
        strcpy(pStrInfo->reader, ptr);
        lens = strlen(pStrInfo->reader);
        ptr += (lens + 1);
        dwReaders -= (lens + 1);
   //       pStrInfo->hCard = 0;
        posReaderList.count++;
        LOGD("SCardListReaders::Reader Count %d Name:%s\n", posReaderList.count, pStrInfo->reader);
        if(posReaderList.count >= MAX_READER_COUNTS)
        {
            break;
        }
    }
    if (posReaderList.count < 2)
    {
        LOGE("[%d]::reader count = %d!!!\n",__LINE__,posReaderList.count);
        SCardStopDeamon(posContext);
        sleep(3);
        if(posContext != 0)
        {
            SCardReleaseContext(posContext);
            posContext = 0;
        }
        if(tryAgain == 0)
        {
            ret = ERROR_NONE;
            tryAgain = 1;
            LOGE("init try again");
            goto START;
        }
        else
        {
            ret = ERROR_APP_LISTREADER;
            return ret;
        }
    }
    LOGD("SCardListReaders::success!\r\n");


    ret = ERROR_NONE;
    return ret;
   }

   //Release context
   unsigned int Pcscd_Release()
   {
    LOGD("%s",__FUNCTION__);
    unsigned int ret = ERROR_NONE;
    int rv;
    pStrReaderListInfo pStrInfo;

    if(posContext == 0)
    {
        LOGE("SCardReleaseContext::Context has not been created!\n");
        ret = ERROR_NONE;
        return ret;
    }
    memset(&posReaderList, 0, sizeof(StrReaderList));
    rv = SCardReleaseContext(posContext);
    if(rv != SCARD_S_SUCCESS)
    {
        LOGE("SCardReleaseContext::native failed!\n");
        posContext = 0;
        ret = ERROR_APP_CONTEXT;
        return ret;
    }
    posContext = 0;

    LOGD("SCardReleaseContext::success!\n");
    ret = ERROR_NONE;
    return ret;
   }

   //Disconnect card connection
   int PBOC_DisConnect(unsigned int using_reader_id)
   {
    LOGD("%s(%d)",__FUNCTION__,using_reader_id);
    LONG rv;
    pStrReaderListInfo pStrInfo;

    if(using_reader_id < 0 || using_reader_id >= posReaderList.count)
    {
        LOGE("[%d]reader id is wrong(%d)\n",__LINE__, using_reader_id);
   //       Nok_Reason = ErrorReason_PCSCD_READERS_ID;
        return FALSE;
    }

    pStrInfo = &posReaderList.readerList[using_reader_id];

    if(pStrInfo->trans_begin == 1)
    {
        rv = SCardEndTransaction(pStrInfo->hCard,SCARD_LEAVE_CARD);
        if (rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            LOGE("[%d]::SCardEndTransaction: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
        }
        pStrInfo->trans_begin = 0;
    }

    if(pStrInfo->hCard != 0)
    {
        rv = SCardDisconnect(pStrInfo->hCard,SCARD_UNPOWER_CARD);
        if (rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            LOGE("[%d]::SCardDisconnect: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
        }
        pStrInfo->hCard = 0;
    }


    return TRUE;
   }
   int PBOC_OpenCard(unsigned char* pAtr,unsigned int * pAtrlen)
   {
    int ret;

    LOGD("%s",__FUNCTION__);

    //Check card
    DWORD dwReaderLen, dwState, dwProt, dwAtrLen;
    BYTE pbAtr[MAX_ATR_SIZE] = "";
    char pbReader[MAX_READERNAME] = "";

    pStrReaderListInfo pStrInfo;
    SCARD_READERSTATE rgReaderStates[1];

    pStrInfo = &posReaderList.readerList[posReaderId];
    dwAtrLen = sizeof(pbAtr);
    dwReaderLen = sizeof(pbReader);

    smart_card_type = 0;
    rgReaderStates[0].szReader =  posReaderList.readerList[posReaderId].reader;
    rgReaderStates[0].dwCurrentState = SCARD_STATE_UNAWARE;
    //Check for card insertion
    ret = SCardGetStatusChange(posContext,INFINITE,rgReaderStates,1);
    if (ret != SCARD_S_SUCCESS)
    {
        printf("SCardGetStatusChange err\r\n");
        return -1;
    }
   LOGD("SCardGetStatusChange ok\r\n");
   LOGD("rgReaderStates[0].dwEventState == %ld\r\n",rgReaderStates[0].dwEventState);        
    if(rgReaderStates[0].dwEventState & SCARD_STATE_PRESENT)//Stuck in
    {
        ret= SCardConnect(posContext, pStrInfo->reader, SCARD_SHARE_SHARED,
                    SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &pStrInfo->hCard, &posActiveProtocol[posReaderId]);
        if (ret == SCARD_S_SUCCESS) 
        {
            ret = SCardStatus(pStrInfo->hCard, pbReader, &dwReaderLen, &dwState, &dwProt,pbAtr, &dwAtrLen);
            if (ret != SCARD_S_SUCCESS)
            {
                return -1;
            }   
            MyPrintfBuff("CardAtr:",pbAtr, dwAtrLen);
            //judge ATR Is it a non-contact card?  
            if((pbAtr[2] == 0x81 && pbAtr[3] == 0x01)||(pbAtr[2] == 0x11 && pbAtr[3] == 0x01) )
            {
                smart_card_type = 2;
            }
            //Judgment of ATR Non-contact Cards  
            else if((pbAtr[1] == 0x70 && pbAtr[2] == 0x11) )
            {
                smart_card_type = 3;
            }
            //Determine whether ATR is a contact card?   
            else if(!((pbAtr[2] == 0x81 && pbAtr[3] == 0x01)||(pbAtr[2] == 0x11 && pbAtr[3] == 0x01) ||(pbAtr[1] == 0x60 && pbAtr[2] == 0x00))) 
            {
                smart_card_type = 1;    
            }
            else
            {
                smart_card_type = -1;
            }
            switch(posActiveProtocol[posReaderId])
            {
                case SCARD_PROTOCOL_T0:
                    pStrInfo->pioSendPci = *SCARD_PCI_T0;
                    LOGD("using T0");
                    break;
                case SCARD_PROTOCOL_T1:
                    pStrInfo->pioSendPci = *SCARD_PCI_T1;
                    LOGD("using T1");
                    break;
                default:
                    LOGE("Unknown protocol. No card present?\n");
            }
            MyMemcpy(pAtr,pbAtr, dwAtrLen);
            *pAtrlen = dwAtrLen;
        }
        else
        {

        }

    }

    return smart_card_type;

   }

   //Establish card connection and prepare for transmission
   int PBOC_Connect(int using_reader_id)
   {
    LOGD("%s(%d)",__FUNCTION__,using_reader_id);
    LONG rv;
    int ret = TRUE;
    DWORD dwReaderLen, dwState, dwProt, dwAtrLen;
    BYTE pbAtr[MAX_ATR_SIZE] = "";
    char pbReader[MAX_READERNAME] = "";
    pStrReaderListInfo pStrInfo;
    unsigned int i;
    unsigned int counts = 0;

    if(using_reader_id < 0 || using_reader_id >= posReaderList.count)
    {
        LOGE("[%d]reader id is wrong(%d)\n",__LINE__, using_reader_id);
        ret = FALSE;
   //       Nok_Reason = ErrorReason_PCSCD_READERS_ID;
        goto ERROR;
    }
    pStrInfo = &posReaderList.readerList[using_reader_id];

    /* connect to a reader (even without a card) */
    LOGD("[%d]Using reader: %s\n",__LINE__, posReaderList.readerList[using_reader_id].reader);

    while(1)
    {
        if(posReaderList.readerList[using_reader_id].hCard != 0)
        {
            LOGD("connected(%d)",(int)posActiveProtocol[using_reader_id]);
            rv = SCARD_S_SUCCESS;
            LOGD("SCardReconnect");
            rv = SCardReconnect(posReaderList.readerList[using_reader_id].hCard, /*SCARD_SHARE_SHARED*/SCARD_SHARE_SHARED, 
                SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,SCARD_RESET_CARD, (DWORD*)&posActiveProtocol);
        }
        else
        {
            LOGD("connect");
            rv = SCardConnect(posContext, posReaderList.readerList[using_reader_id].reader, /*SCARD_SHARE_EXCLUSIVE*/SCARD_SHARE_SHARED,
                SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &posReaderList.readerList[using_reader_id].hCard, &posActiveProtocol[using_reader_id]);
        }
        if (rv != SCARD_S_SUCCESS)
        {
            LOGE("SCardConnect %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
            counts++;
            if(rv == SCARD_E_SHARING_VIOLATION)
            {
                if(counts < 10)
                {
                    usleep(500*1000);
                    LOGE("reader is busy,wait 500ms try again(%d)",counts);
                    continue;
                }
            }
            else if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
                if(counts < 2)
                {
                    LOGE("try again(%d)",counts);
                    continue;
                }
            }
            ret = FALSE;
   //           Nok_Reason = ErrorReason_PCSCD_CONNECT_READERS;
            goto ERROR_NOTHING;
        }
        else
        {
            break;
        }
    }
    pStrInfo->dwActiveProtocol = posActiveProtocol[using_reader_id];
    /* get card status */
    dwAtrLen = sizeof(pbAtr);
    dwReaderLen = sizeof(pbReader);

    rv = SCardStatus(posReaderList.readerList[using_reader_id].hCard, pbReader, &dwReaderLen, &dwState, &dwProt,
        pbAtr, &dwAtrLen);
    if (rv != SCARD_S_SUCCESS)
    {
        LOGE("SCardStatus %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
        ret = FALSE;
   //       Nok_Reason = ErrorReason_PCSCD_GET_READERS_STATUS;
        goto ERROR;
    }
    LOGD(" State: 0x%04lX\n", dwState);

    if (dwState & SCARD_ABSENT)
    {
        LOGE("No card inserted\n");
        ret = FALSE;
   //       Nok_Reason = ErrorReason_PCSCD_NO_CARDS_INSIDE;
        goto ERROR;
    }

    switch(posActiveProtocol[using_reader_id])
    {
        case SCARD_PROTOCOL_T0:
            pStrInfo->pioSendPci = *SCARD_PCI_T0;
            LOGD("using T0");
            break;
        case SCARD_PROTOCOL_T1:
            pStrInfo->pioSendPci = *SCARD_PCI_T1;
            LOGD("using T1");
            break;
        default:
            LOGE("Unknown protocol. No card present?\n");
            ret = FALSE;
   //           Nok_Reason = ErrorReason_PCSCD_NO_PROTOCOLS_SELECT;
            goto ERROR;
    }
    rv = SCardBeginTransaction(posReaderList.readerList[using_reader_id].hCard);
    if (rv != SCARD_S_SUCCESS)
    {
        if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
        {
            Reset_Reader();
        }
        LOGE("SCardBeginTransaction %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
        ret = FALSE;
   //       Nok_Reason = ErrorReason_PCSCD_BEGIN_TRANSACTION;
        goto ERROR;
    }
    pStrInfo->trans_begin = 1;
    LOGD("SCardBeginTransaction::success!\n");
    return ret;
   ERROR:
    rv = SCardDisconnect(posReaderList.readerList[using_reader_id].hCard,SCARD_UNPOWER_CARD);
    if (rv != SCARD_S_SUCCESS)
    {
        if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
        {
            Reset_Reader();
        }
        LOGE("[%d]::SCardDisconnect: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
    }
   ERROR_NOTHING:
    return ret;
   }
   //Connect the card and get ATR
   int PBOC_PowerOn(int using_reader_id,unsigned char* pAtr,unsigned int * pAtrlen)
   {
    LOGD("%s",__FUNCTION__);
    LONG rv;
    int ret = TRUE;
    DWORD dwReaderLen, dwState, dwProt, dwAtrLen;
    BYTE pbAtr[MAX_ATR_SIZE] = "";
    char pbReader[MAX_READERNAME] = "";
    pStrReaderListInfo pStrInfo;
    unsigned int i;
    unsigned int counts = 0;

    if(using_reader_id < 0 || using_reader_id >= posReaderList.count)
    {
        LOGE("[%d]reader id is wrong(%d)\n",__LINE__, using_reader_id);
        ret = FALSE;
        return ret;
    }
    pStrInfo = &posReaderList.readerList[using_reader_id];

    /* connect to a reader (even without a card) */
    LOGD("[%d]Using reader: %s\n",__LINE__, posReaderList.readerList[using_reader_id].reader);

    while(1)
    {
        if(pStrInfo->hCard != 0)
        {
            LOGD("SCardConnect(%d)",(int)posActiveProtocol[using_reader_id]);
            rv = SCARD_S_SUCCESS;
            //LOGD("SCardReconnect");
            //rv = SCardReconnect(pStrInfo->hCard, /*SCARD_SHARE_SHARED*/SCARD_SHARE_EXCLUSIVE, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
            //      SCARD_RESET_CARD, &dwposActiveProtocol);
        }
        else
        {
            LOGD("SCardConnect");
            rv = SCardConnect(posContext, pStrInfo->reader, /*SCARD_SHARE_SHARED*/SCARD_SHARE_SHARED,
                SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &pStrInfo->hCard, &posActiveProtocol[using_reader_id]);
        }
        if (rv != SCARD_S_SUCCESS)
        {
            LOGE("SCardConnect %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
            counts++;
            if(rv == SCARD_E_SHARING_VIOLATION)
            {
                if(counts < 10)
                {
                    usleep(500*1000);
                    LOGE("reader is busy,wait 500ms try again(%d)",counts);
                    continue;
                }
            }
            else if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                usleep(2000*1000);
                Pcscd_Init();
                if(counts < 2)
                {
                    LOGE("try again(%d)",counts);
                    continue;
                }
            }
            ret = FALSE;
            goto ERROR_NOTHING;
        }
        else
        {
            break;
        }
    }
    pStrInfo->dwActiveProtocol = posActiveProtocol[using_reader_id];
    /* get card status */
    dwAtrLen = sizeof(pbAtr);
    dwReaderLen = sizeof(pbReader);
    //Get card status information
    rv = SCardStatus(pStrInfo->hCard, pbReader, &dwReaderLen, &dwState, &dwProt,
        pbAtr, &dwAtrLen);
    if (rv != SCARD_S_SUCCESS)
    {
        LOGE("SCardStatus %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
        ret = FALSE;
        goto ERROR;
    }
    LOGD(" State: 0x%04lX\n", dwState);

    if (dwState & SCARD_ABSENT)
    {
        LOGE("No card inserted\n");
        ret = FALSE;
        goto ERROR;
    }

    switch(posActiveProtocol[using_reader_id])
    {
        case SCARD_PROTOCOL_T0:
            pStrInfo->pioSendPci = *SCARD_PCI_T0;
            LOGD("using T0");
            break;
        case SCARD_PROTOCOL_T1:
            pStrInfo->pioSendPci = *SCARD_PCI_T1;
            LOGD("using T1");
            break;
        default:
            LOGE("Unknown protocol. No card present?\n");
            ret = FALSE;
            goto ERROR;
    }
    MyMemcpy(pbAtr,pAtr,dwAtrLen);
    *pAtrlen = dwAtrLen;
    //Prepare for transmission status
    rv = SCardBeginTransaction(pStrInfo->hCard);
    if (rv != SCARD_S_SUCCESS)
    {
        if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
        {
            Reset_Reader();
        }
        LOGE("SCardBeginTransaction %s (0x%lX)\n" , pcsc_stringify_error(rv), rv);
        ret = FALSE;
        goto ERROR;
    }
    pStrInfo->trans_begin = 1;
    LOGD("SCardBeginTransaction::success!\n");
    return ret;
   ERROR:
    rv = SCardDisconnect(pStrInfo->hCard,SCARD_UNPOWER_CARD);
    if (rv != SCARD_S_SUCCESS)
    {
        if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
        {
            Reset_Reader();
        }
        LOGE("[%d]::SCardDisconnect: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
    }
   ERROR_NOTHING:
    return ret;


   }
   int PBOC_PowerOff(int using_reader_id)
   {
    LOGD("%s",__FUNCTION__);
    LONG rv;
    pStrReaderListInfo pStrInfo;

    pStrInfo = &posReaderList.readerList[using_reader_id];
    //End of transmission
    if(pStrInfo->trans_begin == 1)
    {
        rv = SCardEndTransaction(pStrInfo->hCard,SCARD_LEAVE_CARD);
        if (rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                ResetReader();
            }
            LOGE("[%d]::SCardEndTransaction: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
        }
        pStrInfo->trans_begin = 0;
    }
    //Disconnect
    if(posReaderList.readerList[using_reader_id].hCard != 0)
    {
        LOGD("SCardDisconnect");
        rv = SCardDisconnect(pStrInfo->hCard,SCARD_UNPOWER_CARD);
        if (rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            LOGE("[%d]::CloudPos_PowerOff: %s (0x%lX)\n",__LINE__ ,pcsc_stringify_error(rv),rv);
        }
        pStrInfo->hCard = 0;
    }
    return rv;
   }

   int PBOC_APDU_SendAPI(unsigned int using_reader_id,unsigned char * bSendBuffer,unsigned int send_length,unsigned char * bRecvBuffer,unsigned int *length)
   {
    LOGD("%s",__FUNCTION__);
    LONG rv;
    int ret = 0;
    unsigned int i;
    unsigned char temp[64] = {0};
    //char debug_buffer[512] = {0};

    pStrReaderListInfo pStrInfo;

    if(using_reader_id < 0 || using_reader_id >= posReaderList.count)
    {
        LOGE("[%d]reader id is wrong(%d)\n",__LINE__, using_reader_id);
        ret = FALSE;
        goto SEND_ERROR;
    }
   #ifdef SE_KL81
    if(send_length%64 == 54)
    {
        LOGI("fill 0xFFFF");
        bSendBuffer[send_length] = 0xFF;
        bSendBuffer[send_length+1] = 0xFF;
        send_length += 2;
    }
   #endif
    pStrInfo = &posReaderList.readerList[using_reader_id];

    /* APDU select applet */

    LOGD("[%d]::PBOC_APDU_SendAPI()::Dev send to card:\n",__LINE__);
    if(send_length >= 256)
    {
        LOGD("send_length is bigger then print buffer length");
    }
    else
    {
        MyPrintfBuff("APDU_send:",bSendBuffer, send_length);
    }
    *length = MAX_BUFFER_SIZE_SZ;
    MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
    if(memcmp(&pStrInfo->pioSendPci,SCARD_PCI_T0,sizeof(pStrInfo->pioSendPci)) == 0)
    {
        LOGD("using T0");
    }
    else if(memcmp(&pStrInfo->pioSendPci,SCARD_PCI_T1,sizeof(pStrInfo->pioSendPci)) == 0)
    {
        LOGD("using T1");
    }
    else
    {
        LOGD("using None");
    }
    rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer, (DWORD)send_length,
                            &pStrInfo->pioRecvPci, bRecvBuffer, (DWORD *)length);
    if(rv != SCARD_S_SUCCESS)
    {
        if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
        {
            Reset_Reader();
        }
        MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
        LOGE("SCardTransmit error,try again!(0x%08x)(%d)",(unsigned int)rv,*length);
        snprintf(temp,sizeof(temp),"SCardTransmit error(0x%x)",(unsigned int)rv);
        sleep(1);
        *length = MAX_BUFFER_SIZE_SZ;
        rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer, (DWORD )send_length,
                                &pStrInfo->pioRecvPci, bRecvBuffer,(DWORD *) length);

        if(rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            LOGE("(rv = 0x%08x)",(unsigned int)rv);
            snprintf(temp,sizeof(temp),"SCardTransmit error again(0x%x)",(unsigned int)rv);
            ret = FALSE;
            goto SEND_ERROR;
        }
    }
    LOGD("[%d]::Card response to dev:\n",__LINE__);
    if(*length >= 5120)
    {
        LOGD("length is bigger then print buffer length.");
    }
    else
    {
        MyPrintfBuff("APDU_Recv:",bRecvBuffer, *length);
    }
    if(*length > 54 && (*length+10)%64 == 2 && (bRecvBuffer[*length-2] == 0xFF && bRecvBuffer[*length-1] == 0xFF))
    {
        LOGD("clean FFFF");
        (*length) -= 2;
    }

   PRASE_CARD_RETURN_STATUS:
    if(bRecvBuffer[*length-2] == 0x61)
    {
        LOGD("[%d]::Dev 0x61 to card:\n",__LINE__);
        memset(bSendBuffer,0x00,MAX_BUFFER_SIZE_SZ);
        usleep(100*1000);
        LOGD("[%d]::Card with the remaining returned to %02x%02x\n",__LINE__,bRecvBuffer[0],bRecvBuffer[1]);
        send_length = 5;//00 C0 00 00 2A
        memcpy(bSendBuffer, "\x00\xC0\x00\x00",send_length-1);

        bSendBuffer[send_length-1] = bRecvBuffer[*length-1];

        LOGD("[%d]::Dev send to card:\n",__LINE__);
        MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
        *length = MAX_BUFFER_SIZE_SZ;
        rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer, (DWORD )send_length,
            &pStrInfo->pioRecvPci, bRecvBuffer, (DWORD *)length);
        MyPrintfBuff("APDU_Recv:",bRecvBuffer, *length);
        if(rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
            LOGE("SCardTransmit error,try again!");
            snprintf(temp,sizeof(temp),"SCardTransmit error(0x%x)",(unsigned int)rv);
            *length = MAX_BUFFER_SIZE_SZ;
            sleep(1);
            rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer, (DWORD )send_length,
                &pStrInfo->pioRecvPci, bRecvBuffer, (DWORD *)length);
            if(rv != SCARD_S_SUCCESS)
            {
                if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
                {
                    Reset_Reader();
                }
                LOGE("SCardTransmit error!");
                snprintf(temp,sizeof(temp),"SCardTransmit error again(0x%x)",(unsigned int)rv);
                ret = FALSE;
                goto SEND_ERROR;
            }
        }
        LOGD("[%d]::Card response to dev:\n",__LINE__);
        //memset(debug_buffer,0,sizeof(debug_buffer));
        if(*length >= 5120)
        {
            LOGE("length is bigger then print buffer length");
        }
        else
        {
        //  myAsciiToHex(bRecvBuffer, debug_buffer, *length);
            MyPrintfBuff("APDU_Recv:",bRecvBuffer, *length);

        }
        if(*length > 54 && (*length+10)%64 == 2 && (bRecvBuffer[*length-2] == 0xFF && bRecvBuffer[*length-1] == 0xFF))
        {
            LOGD("clean FFFF");
            (*length) -= 2;
        }
    }
    else if(bRecvBuffer[*length-2] == 0x6C)
    {
        LOGD("[%d]::Dev 0x6C to card:\n",__LINE__);
        bSendBuffer[send_length-1] = bRecvBuffer[*length-1];
        MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
        rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer,(DWORD ) send_length,
            &pStrInfo->pioRecvPci, bRecvBuffer, (DWORD *)length);
        if(rv != SCARD_S_SUCCESS)
        {
            if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
            {
                Reset_Reader();
            }
            MyMemset(bRecvBuffer,0x00,MAX_BUFFER_SIZE_SZ);
            LOGE("SCardTransmit error,try again!");
            *length = MAX_BUFFER_SIZE_SZ;
            sleep(1);
            rv = SCardTransmit(pStrInfo->hCard, &pStrInfo->pioSendPci, bSendBuffer, (DWORD )send_length,
                &pStrInfo->pioRecvPci, bRecvBuffer, (DWORD*)length);
            if(rv != SCARD_S_SUCCESS)
            {
                if(rv == SCARD_E_NO_SERVICE || rv == SCARD_E_UNKNOWN_READER)
                {
                    Reset_Reader();
                }
   //               Nok_Reason = ErrorReason_PCSCD_TRANSMIT;
                ret = FALSE;
                goto SEND_ERROR;
            }
        }
        if(*length > 54 && (*length+10)%64 == 2 && (bRecvBuffer[*length-2] == 0xFF && bRecvBuffer[*length-1] == 0xFF))
        {
            LOGD("clean FFFF");
            (*length) -= 2;
        }
        goto PRASE_CARD_RETURN_STATUS;
    }
    else if((bRecvBuffer[*length-2] == 0x6A )&&( bRecvBuffer[*length-1] == 0x83))
    {
        LOGD("[%d]::Card response 6A83",__LINE__);
    }
    else if((bRecvBuffer[*length-2] != 0x90)||(bRecvBuffer[*length-1] != 0x00))
    {
        LOGE("Cards respont isn't 0x9000(0x%02X%02X)!\n",bRecvBuffer[*length-2],bRecvBuffer[*length-1]);
    }
    else
    {
        //LOGD("[%d]::Card response to dev:\n",__LINE__);
        //memset(debug_buffer,0,sizeof(debug_buffer));
        if(*length >= 5120)
        {
            LOGE("length is bigger then print buffer length");
        }
        else
        {
            //myAsciiToHex(bRecvBuffer, debug_buffer, *length);
            //MyPrintfBuff("APDU_Recv:",bRecvBuffer, *length);
        }
    }
   SEND_ERROR:
    return ret;
   }

//main.c
//Using test program to communicate between PSAM card and ICC card
ret = Pcscd_Init();
if(ret != 0)//Card reader not found
{
    return -1;
}
while(1)
{
    printf("Please input command:\n");
    printf("################################\n");
    printf("'0' is select PSAM CARD\n");
    printf("'1' is select ICC CARD\n");
    printf("'quit' is return\n");
    printf("'break' is go back\n");
    printf("################################\n");
    MyMemset(camd_buf,0,sizeof(camd_buf));
    scanf("%s",camd_buf);
    getchar();
    if(MyMemcmp("0",camd_buf,MyStrlen("0")) == 0)
    {
        while(1)
        {
            printf("Please input APDU:\n");
            MyMemset(camd_buf,0,sizeof(camd_buf));
            scanf("%s",camd_buf);
            getchar();
            if(MyMemcmp("break",camd_buf,MyStrlen("break")) == 0)
                break;
            MyStrToHex((camd_buf),send_buf);
            ret = CloudPos_SendToPcscd((unsigned char *)send_buf,0,recv_buf,&recv_len);

            MyPrintfBuff("recv_buf::",recv_buf,recv_len);
        }
    }
    else if(memcmp("1",camd_buf,MyStrlen("1")) == 0)
    {
        ret = PBOC_Connect(1);
        if(ret == FALSE)
        {
            printf("Connect(1) error Please try again!\n");
        }
        else
        {
            printf("Connect(1) sucess\n");
            while(1)
            {
                printf("Please input APDU:\n");
                MyMemset(camd_buf,0,sizeof(camd_buf));
                scanf("%s",camd_buf);
                getchar();
                if(MyMemcmp("break",camd_buf,MyStrlen("break")) == 0)
                    break;
                send_len = MyStrToHex((camd_buf),send_buf);

                ret = PBOC_APDU_SendAPI(1,send_buf,send_len,recv_buf,&recv_len);
                MyPrintfBuff("recv_buf::",recv_buf,recv_len);

            }
        }
        PBOC_DisConnect(1);
    }
    else if(MyMemcmp("quit",camd_buf,MyStrlen("quit")) == 0)
    {
        break;
    }
}
Pcscd_Release();

Keywords: Linux Database

Added by Texan on Thu, 16 May 2019 16:28:44 +0300