/*
 *  Copyright (c) 2005-2026, Karsten Ohme
 *  This file is part of GlobalPlatform.
 *
 *  GlobalPlatform 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.
 *
 *  GlobalPlatform 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 GlobalPlatform.  If not, see <https://www.gnu.org/licenses/>.
 */

/*! \mainpage GlobalPlatform PC/SC Connection Plugin
*
* \author Karsten Ohme
* \section intro_sec Introduction
* This plugin uses PC/SC to connect to a smart card.
*
*/

#include <globalplatform/connectionplugin.h>
#include "gppcscconnectionplugin.h"
#include <globalplatform/debug.h>
#include <globalplatform/error.h>
#include <globalplatform/errorcodes.h>
#include <globalplatform/unicode.h>
#include <globalplatform/stringify.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "util.h"

#ifndef SCARD_CTL_CODE
#define SCARD_CTL_CODE(code) (0x42000000 + (code))
#endif

#ifndef CM_IOCTL_GET_FEATURE_REQUEST
#define CM_IOCTL_GET_FEATURE_REQUEST SCARD_CTL_CODE(3400)
#endif

#ifndef FEATURE_GET_TLV_PROPERTIES
#define FEATURE_GET_TLV_PROPERTIES 0x12
#endif

#ifndef PCSCv2_PART10_PROPERTY_dwMaxAPDUDataSize
#define PCSCv2_PART10_PROPERTY_dwMaxAPDUDataSize 0x0A
#endif


#define CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)	if (cardContext.librarySpecific == NULL) { OPGP_ERROR_CREATE_ERROR(status, OPGP_PL_ERROR_NO_CARD_CONTEXT_INITIALIZED, OPGP_PL_stringify_error(OPGP_PL_ERROR_NO_CARD_CONTEXT_INITIALIZED)); goto end;}

#define CHECK_CARD_INFO_INITIALIZATION(cardInfo, status)	if (cardInfo.librarySpecific == NULL) { OPGP_ERROR_CREATE_ERROR(status, OPGP_PL_ERROR_NO_CARD_INFO_INITIALIZED, OPGP_PL_stringify_error(OPGP_PL_ERROR_NO_CARD_INFO_INITIALIZED)); goto end;}

/**
* Handles the error status for the result.
*/
#define HANDLE_STATUS(status, result) if (result != SCARD_S_SUCCESS) {\
	OPGP_ERROR_CREATE_ERROR(status,result,OPGP_PL_stringify_error((DWORD)result));\
	}\
	else {\
	OPGP_ERROR_CREATE_NO_ERROR(status);\
	}

/**
* Convenience function to get the SCARDCONTEXT from a pointer to OPGP_CARD_CONTEXT struct.
* @param pContext The pointer to a OPGP_CARD_CONTEXT struct.
*/
#define GET_SCARDCONTEXT_P(pContext) ((PCSC_CARD_CONTEXT_SPECIFIC *)(pContext->librarySpecific))->cardContext

/**
* Convenience function to get the SCARDCONTEXT from a OPGP_CARD_CONTEXT struct.
* @param context The OPGP_CARD_CONTEXT struct.
*/
#define GET_SCARDCONTEXT(context) ((PCSC_CARD_CONTEXT_SPECIFIC *)(context.librarySpecific))->cardContext

/**
* Convenience function to get a reference to the PCSC_CARD_INFO_SPECIFIC from a pointer to OPGP_CARD_INFO struct.
* @param pCardInfo The pointer to a OPGP_CARD_INFO struct.
*/
#define GET_PCSC_CARD_INFO_SPECIFIC_P(pCardInfo) ((PCSC_CARD_INFO_SPECIFIC *)(pCardInfo->librarySpecific))

/**
* Convenience function to get a reference to the PCSC_CARD_INFO_SPECIFIC from a OPGP_CARD_INFO struct.
* @param pCardInfo The pointer to a OPGP_CARD_INFO struct.
*/
#define GET_PCSC_CARD_INFO_SPECIFIC(cardInfo) ((PCSC_CARD_INFO_SPECIFIC *)(cardInfo.librarySpecific))

static DWORD read_uint32_be(const BYTE *data) {
	return ((DWORD)data[0] << 24) | ((DWORD)data[1] << 16) | ((DWORD)data[2] << 8) | data[3];
}

static DWORD read_uint32_le(const BYTE *data) {
	return ((DWORD)data[3] << 24) | ((DWORD)data[2] << 16) | ((DWORD)data[1] << 8) | data[0];
}

static DWORD read_pcsc_tlv_property_uint32(const BYTE *data) {
	DWORD value = read_uint32_be(data);
	if (value > 0 && value <= 65536) {
		return value;
	}
	value = read_uint32_le(data);
	if (value > 0 && value <= 65536) {
		return value;
	}
	return 0;
}

/**
* Memory is allocated in this method for the card context. It must be freed with a call to #OPGP_PL_release_context.
* \param cardContext [out] The returned OPGP_CARDCONTEXT.
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code  and error message are contained in the OPGP_ERROR_STATUS struct
*/
OPGP_ERROR_STATUS OPGP_PL_establish_context(OPGP_CARD_CONTEXT *cardContext) {
	OPGP_ERROR_STATUS status;
	memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
	LONG result = SCARD_S_SUCCESS;
	OPGP_LOG_START(_T("OPGP_PL_establish_context"));
	cardContext->librarySpecific = malloc(sizeof(PCSC_CARD_CONTEXT_SPECIFIC));
	if (cardContext->librarySpecific == NULL) {
		OPGP_ERROR_CREATE_ERROR(status, ENOMEM, OPGP_stringify_error(ENOMEM));
		goto end;
	}
	result = SCardEstablishContext( SCARD_SCOPE_USER,
		NULL,
		NULL,
		&GET_SCARDCONTEXT_P(cardContext)
		);
#ifdef _WIN32
	if (result == SCARD_E_NO_SERVICE || result == SCARD_E_SERVICE_STOPPED) {
		OPGP_log_Msg(_T("SCardEstablishContext(SCARD_SCOPE_USER) failed, retrying SCARD_SCOPE_SYSTEM"));
		result = SCardEstablishContext( SCARD_SCOPE_SYSTEM,
			NULL,
			NULL,
			&GET_SCARDCONTEXT_P(cardContext)
			);
	}
#endif
	if (result != SCARD_S_SUCCESS && cardContext->librarySpecific != NULL) {
		free(cardContext->librarySpecific);
		cardContext->librarySpecific = NULL;
	}
	HANDLE_STATUS(status, result);
end:
	OPGP_LOG_END(_T("OPGP_PL_establish_context"), status);
	return status;
}

/**
* \param cardContext [in, out] The valid OPGP_CARDCONTEXT returned by establish_context()
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code  and error message are contained in the OPGP_ERROR_STATUS struct
*/
OPGP_ERROR_STATUS OPGP_PL_release_context(OPGP_CARD_CONTEXT *cardContext) {
	OPGP_ERROR_STATUS status;
	memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
	LONG result = SCARD_S_SUCCESS;
	OPGP_LOG_START(_T("OPGP_PL_release_context"));
	CHECK_CARD_CONTEXT_INITIALIZATION((*cardContext), status)
		result = SCardReleaseContext(GET_SCARDCONTEXT((*cardContext)));
	HANDLE_STATUS(status, result);
	// frees the allocated memory
	if (cardContext->librarySpecific != NULL) {
		free(cardContext->librarySpecific);
		cardContext->librarySpecific = NULL;
	}
end:
	OPGP_LOG_END(_T("OPGP_PL_release_context"), status);
	return status;
}

/**
* \param cardContext [in] The valid OPGP_CARDCONTEXT returned by establish_context()
* \param readerNames [out] The reader names will be a multi-string and separated by a NULL character and ended by a double NULL.
*  (ReaderA\\0ReaderB\\0\\0). If this value is NULL, list_readers ignores the buffer length supplied in
*  readerNamesLength, writes the length of the multi-string that would have been returned if this parameter
*  had not been NULL to readerNamesLength.
* \param readerNamesLength [in, out] The length of the multi-string including all trailing null characters.
* \param presentOnly If non-zero, only readers with a smart card inserted are returned.
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code and error message are contained in the OPGP_ERROR_STATUS struct
*/
OPGP_ERROR_STATUS OPGP_PL_list_readers(OPGP_CARD_CONTEXT cardContext, OPGP_STRING readerNames, PDWORD readerNamesLength, DWORD presentOnly) {
    OPGP_ERROR_STATUS status;
    memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
    LONG result = SCARD_S_SUCCESS;
    DWORD readersSize = 0;
    OPGP_STRING readers = NULL;
    OPGP_STRING filtered = NULL;
    OPGP_LOG_START(_T("OPGP_PL_list_readers"));
    CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)
        result = SCardListReaders(GET_SCARDCONTEXT(cardContext), NULL, NULL, &readersSize );
    if ( SCARD_S_SUCCESS != result ) {
        goto end;
    }
#ifdef DEBUG
    OPGP_log_Msg(_T("OPGP_PL_list_readers: readerSize: %d"), readersSize);
#endif
    if (readerNames == NULL && !presentOnly) {
        *readerNamesLength = readersSize;
        result = SCARD_S_SUCCESS;
        goto end;
    }
    readers = (OPGP_STRING)malloc(sizeof(TCHAR)*readersSize);
    if (readers == NULL) {
        result = ENOMEM;
        goto end;
    }

    result = SCardListReaders(GET_SCARDCONTEXT(cardContext), NULL, readers, &readersSize);
    if ( SCARD_S_SUCCESS != result ) {
        goto end;
    }
    if (!presentOnly) {
        if (readerNames == NULL) {
            *readerNamesLength = readersSize;
            result = SCARD_S_SUCCESS;
            goto end;
        }
        if (*readerNamesLength < readersSize) {
            _tcsncpy(readerNames, readers, (*readerNamesLength)-2);
            *(readerNames+(*readerNamesLength)-2) = _T('\0');
            *(readerNames+(*readerNamesLength)-1) = _T('\0');
            *readerNamesLength = readersSize;
        }
        else {
            memcpy(readerNames, readers, sizeof(TCHAR)*readersSize);
            *readerNamesLength = readersSize;
        }
    } else {
        // Build list of only readers with a card present using SCardGetStatusChange
        // Count readers and create state array
        DWORD count = 0;
        for (DWORD j = 0; j < readersSize;) {
            if (readers[j] == _T('\0')) break;
            size_t len = _tcslen(&readers[j]);
            if (len == 0) break;
            count++;
            j += (DWORD)len + 1;
        }
        if (count == 0) {
            // No readers at all
            if (readerNames == NULL) {
                *readerNamesLength = 1; // single terminating null
            } else {
                if (*readerNamesLength > 0) {
                    readerNames[0] = _T('\0');
                }
                *readerNamesLength = 1;
            }
            result = SCARD_S_SUCCESS;
            goto end;
        }
        SCARD_READERSTATE *states = (SCARD_READERSTATE*)calloc(count, sizeof(SCARD_READERSTATE));
        if (!states) { result = ENOMEM; goto end; }
        DWORD idx = 0;
        for (DWORD j = 0; j < readersSize && idx < count;) {
            if (readers[j] == _T('\0')) break;
            size_t len = _tcslen(&readers[j]);
            if (len == 0) break;
            states[idx].szReader = &readers[j];
            states[idx].dwCurrentState = SCARD_STATE_UNAWARE;
            idx++;
            j += (DWORD)len + 1;
        }
        result = SCardGetStatusChange(GET_SCARDCONTEXT(cardContext), 0, states, count);
        if (SCARD_S_SUCCESS != result) {
            free(states);
            goto end;
        }
        // Calculate filtered size (in TCHARs)
        DWORD filteredSize = 0;
        for (DWORD i = 0; i < count; ++i) {
            if (states[i].dwEventState & SCARD_STATE_PRESENT) {
                filteredSize += (DWORD)_tcslen((LPCTSTR)states[i].szReader) + 1;
            }
        }
        if (filteredSize == 0) {
            filteredSize = 2; // double-null for empty multi-string
        } else {
            filteredSize += 1; // add final null to make double-null
        }
        if (readerNames == NULL) {
            *readerNamesLength = filteredSize;
            free(states);
            result = SCARD_S_SUCCESS;
            goto end;
        }
        filtered = (OPGP_STRING)malloc(sizeof(TCHAR) * filteredSize);
        if (!filtered) { free(states); result = ENOMEM; goto end; }
        DWORD outPos = 0;
        for (DWORD i = 0; i < count; ++i) {
            if (states[i].dwEventState & SCARD_STATE_PRESENT) {
                LPCTSTR name = (LPCTSTR)states[i].szReader;
                size_t nlen = _tcslen(name);
                _tcsncpy(&filtered[outPos], name, nlen);
                outPos += (DWORD)nlen;
                filtered[outPos++] = _T('\0');
            }
        }
        // Ensure final double-null terminator
        filtered[outPos++] = _T('\0');
        if (outPos < filteredSize) {
            filtered[outPos++] = _T('\0');
        }
        // Copy to output with possible truncation following previous semantics
        if (*readerNamesLength < filteredSize) {
            _tcsncpy(readerNames, filtered, (*readerNamesLength) - 2);
            *(readerNames + (*readerNamesLength) - 2) = _T('\0');
            *(readerNames + (*readerNamesLength) - 1) = _T('\0');
            *readerNamesLength = filteredSize;
        } else {
            memcpy(readerNames, filtered, sizeof(TCHAR) * filteredSize);
            *readerNamesLength = filteredSize;
        }
        free(states);
    }
end:
    if (readers) {
        free(readers);
    }
    if (filtered) {
        free(filtered);
    }
    HANDLE_STATUS(status, result);
    OPGP_LOG_END(_T("OPGP_PL_list_readers"), status);
    return status;
}

/**
* Memory is allocated in this method for the card context. It must be freed with a call to #OPGP_PL_card_disconnect.
* If something is not working, you may want to change the protocol type.
* \param cardContext [in] The valid OPGP_CARDCONTEXT returned by establish_context()
* \param readerName [in] The name of the reader to connect.
* \param *cardInfo [out] The returned OPGP_CARD_INFO.
* \param protocol [in] The transmit protocol type to use. Can be OPGP_CARD_PROTOCOL_T0 or OPGP_CARD_PROTOCOL_T1 or both ORed.
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code  and error message are contained in the OPGP_ERROR_STATUS struct.
*/
OPGP_ERROR_STATUS OPGP_PL_card_connect(OPGP_CARD_CONTEXT cardContext, OPGP_CSTRING readerName, OPGP_CARD_INFO *cardInfo,
	DWORD protocol) {
		OPGP_ERROR_STATUS status;
		memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
		LONG result = SCARD_S_SUCCESS;
		PCSC_CARD_INFO_SPECIFIC *pcscCardInfo = NULL;
		DWORD activeProtocol = 0;
		DWORD state = 0;
		DWORD dummy = 0;
		BYTE ATR[MAX_ATR_SIZE];
		memset(ATR, 0, sizeof(ATR));
		DWORD ATRLength=MAX_ATR_SIZE;
		TCHAR readerNameTemp[1024];
		memset(readerNameTemp, 0, sizeof(readerNameTemp));
		DWORD readerNameTempLength = 1024;

		OPGP_LOG_START(_T("OPGP_PL_card_connect"));
		CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)

		cardInfo->librarySpecific = malloc(sizeof(PCSC_CARD_INFO_SPECIFIC));
		if (cardInfo->librarySpecific == NULL) {
			OPGP_ERROR_CREATE_ERROR(status, ENOMEM, OPGP_stringify_error(ENOMEM));
			goto error;
		}

		pcscCardInfo = GET_PCSC_CARD_INFO_SPECIFIC_P(cardInfo);

		result = SCardConnect( GET_SCARDCONTEXT(cardContext),
			readerName,
			SCARD_SHARE_SHARED,
			protocol,
			&(pcscCardInfo->cardHandle),
			&activeProtocol );
		if ( SCARD_S_SUCCESS != result ) {
			goto error;
		}

		result = SCardStatus(pcscCardInfo->cardHandle, readerNameTemp, &readerNameTempLength, &state, &dummy, ATR, &ATRLength);
		if ( SCARD_S_SUCCESS != result ) {
			goto error;
		}
		memcpy(cardInfo->ATR, ATR, MAX_ATR_SIZE);
		cardInfo->ATRLength = ATRLength;

		pcscCardInfo->protocol = dummy;
		pcscCardInfo->state = state;

#ifdef DEBUG
		OPGP_log_Msg(_T("OPGP_PL_card_connect: Connected to card in reader %s with protocol %d in card state %d"), readerName, pcscCardInfo->protocol, pcscCardInfo->state);
		OPGP_log_Hex(_T("OPGP_PL_card_connect: Card ATR: "), cardInfo->ATR, cardInfo->ATRLength);
#endif

		cardInfo->logicalChannel = 0;
		goto end;
error:
		if (cardInfo->librarySpecific != NULL) {
			free(cardInfo->librarySpecific);
			cardInfo->librarySpecific = NULL;
		}
end:
		HANDLE_STATUS(status, result);
		OPGP_LOG_END(_T("OPGP_PL_card_connect"), status);
		return status;
}

/**
* \param cardContext [in] The valid OPGP_CARDCONTEXT returned by establish_context()
* \param cardInfo [in, out] The OPGP_CARD_INFO structure returned by card_connect().
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code  and error message are contained in the OPGP_ERROR_STATUS struct.
*/
OPGP_ERROR_STATUS OPGP_PL_card_disconnect(OPGP_CARD_CONTEXT cardContext, OPGP_CARD_INFO *cardInfo) {
	OPGP_ERROR_STATUS status;
	memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
	LONG result = SCARD_S_SUCCESS;
	OPGP_LOG_START(_T("OPGP_PL_card_disconnect"));
	CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)
		CHECK_CARD_INFO_INITIALIZATION((*cardInfo), status)
		result = SCardDisconnect(GET_PCSC_CARD_INFO_SPECIFIC((*cardInfo))->cardHandle, SCARD_RESET_CARD);
	HANDLE_STATUS(status, result);
	// frees the allocated memory
	if (cardInfo->librarySpecific != NULL) {
		free(cardInfo->librarySpecific);
		cardInfo->librarySpecific = NULL;
	}
	cardInfo->ATRLength = 0;
end:
	OPGP_LOG_END(_T("OPGP_PL_card_disconnect"), status);
	return status;
}

/**
* \param cardContext [in] The valid OPGP_CARDCONTEXT returned by establish_context()
* \param cardInfo [in] The OPGP_CARD_INFO structure returned by card_connect().
* \param supported [out] TRUE if the reader/card connection supports extended APDUs.
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs.
*/
OPGP_ERROR_STATUS OPGP_PL_supports_extended_APDU(OPGP_CARD_CONTEXT cardContext, OPGP_CARD_INFO cardInfo, BOOL *supported) {
	OPGP_ERROR_STATUS status;
	LONG result = SCARD_S_SUCCESS;
	BYTE featureBuffer[256];
	DWORD featureBufferLength = sizeof(featureBuffer);
	DWORD propertiesControlCode = 0;
	BYTE propertiesBuffer[256];
	DWORD propertiesBufferLength = sizeof(propertiesBuffer);
	DWORD offset;

	memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
	OPGP_LOG_START(_T("OPGP_PL_supports_extended_APDU"));
	if (supported == NULL) {
		OPGP_ERROR_CREATE_ERROR(status, EINVAL, OPGP_stringify_error(EINVAL));
		goto end;
	}
	*supported = 0;
	CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)
	CHECK_CARD_INFO_INITIALIZATION(cardInfo, status)

	if (GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->protocol == SCARD_PROTOCOL_T0) {
		OPGP_ERROR_CREATE_NO_ERROR(status);
		goto end;
	}

	result = SCardControl(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
			CM_IOCTL_GET_FEATURE_REQUEST,
			NULL,
			0,
			featureBuffer,
			featureBufferLength,
			&featureBufferLength);
	if (result != SCARD_S_SUCCESS) {
		OPGP_ERROR_CREATE_NO_ERROR(status);
		goto end;
	}

	for (offset = 0; offset + 2 <= featureBufferLength;) {
		BYTE tag = featureBuffer[offset];
		BYTE length = featureBuffer[offset + 1];
		if (offset + 2 + length > featureBufferLength) {
			break;
		}
		if (tag == FEATURE_GET_TLV_PROPERTIES && length == 4) {
			propertiesControlCode = read_uint32_be(featureBuffer + offset + 2);
			break;
		}
		offset += 2 + length;
	}
	if (propertiesControlCode == 0) {
		OPGP_ERROR_CREATE_NO_ERROR(status);
		goto end;
	}

	result = SCardControl(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
			propertiesControlCode,
			NULL,
			0,
			propertiesBuffer,
			propertiesBufferLength,
			&propertiesBufferLength);
	if (result != SCARD_S_SUCCESS) {
		OPGP_ERROR_CREATE_NO_ERROR(status);
		goto end;
	}

	for (offset = 0; offset + 2 <= propertiesBufferLength;) {
		BYTE tag = propertiesBuffer[offset];
		BYTE length = propertiesBuffer[offset + 1];
		if (offset + 2 + length > propertiesBufferLength) {
			break;
		}
		if (tag == PCSCv2_PART10_PROPERTY_dwMaxAPDUDataSize && length == 4) {
			DWORD maxApduDataSize = read_pcsc_tlv_property_uint32(propertiesBuffer + offset + 2);
			*supported = maxApduDataSize > 255;
			break;
		}
		offset += 2 + length;
	}

	OPGP_ERROR_CREATE_NO_ERROR(status);
end:
	OPGP_LOG_END(_T("OPGP_PL_supports_extended_APDU"), status);
	return status;
}

/**
* If the transmission is successful then the APDU status word is returned as errorCode in the OPGP_ERROR_STATUS structure.
* \param cardContext [in] The valid OPGP_CARDCONTEXT returned by establish_context()
* \param cardInfo [in] The OPGP_CARD_INFO structure returned by card_connect().
* \param capdu [in] The command APDU.
* \param capduLength [in] The length of the command APDU.
* \param rapdu [out] The response APDU.
* \param rapduLength [in, out] The length of the the response APDU.
* \return OPGP_ERROR_STATUS struct with error status OPGP_ERROR_STATUS_SUCCESS if no error occurs, otherwise error code  and error message are contained in the OPGP_ERROR_STATUS struct.
*/
OPGP_ERROR_STATUS OPGP_PL_send_APDU(OPGP_CARD_CONTEXT cardContext, OPGP_CARD_INFO cardInfo, PBYTE capdu, DWORD capduLength, PBYTE rapdu, PDWORD rapduLength) {
	OPGP_ERROR_STATUS status;
	memset(&status, 0, sizeof(OPGP_ERROR_STATUS));
	// modified for managing all 4 cases with automatic APDU chaining

	DWORD result = SCARD_S_SUCCESS;
	BYTE caseAPDU = 0;
	DWORD lc = 0;
	DWORD le = 0;
	BYTE la = 0;

	DWORD offset = 0;

	PBYTE responseData = NULL;
	DWORD responseDataLength = *rapduLength;
	DWORD tempDataLength = 0;

	OPGP_LOG_START(_T("OPGP_PL_send_APDU"));
	CHECK_CARD_CONTEXT_INITIALIZATION(cardContext, status)
		CHECK_CARD_INFO_INITIALIZATION(cardInfo, status)
		responseData = (PBYTE)malloc(sizeof(BYTE)*responseDataLength);
	if (responseData == NULL) {
		result = ENOMEM;
		HANDLE_STATUS(status, result);
		goto end;
	}

	// if T=1 or else T=0
	if (GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->protocol == SCARD_PROTOCOL_T1) {

		// T=1 transmission

		result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
			SCARD_PCI_T1,
			capdu,
			capduLength,
			NULL,
			responseData,
			&responseDataLength
			);
		if (SCARD_S_SUCCESS != result) {
			HANDLE_STATUS(status, result);
			goto end;
		} // if ( SCARD_S_SUCCESS != result)
		offset += responseDataLength - 2;
	} else {
		if (parse_apdu_case(capdu, capduLength, &caseAPDU, &lc, &le)) {
			result = OPGP_ERROR_UNRECOGNIZED_APDU_COMMAND;
			HANDLE_STATUS(status, result);
			goto end;
		}
		if (caseAPDU == 4) {
			capduLength--;
		}

		// T=0 transmission (first command)

		responseDataLength = *rapduLength;
		result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
			SCARD_PCI_T0,
			capdu,
			capduLength,
			NULL,
			responseData,
			&responseDataLength
			);
		if ( SCARD_S_SUCCESS != result) {
			HANDLE_STATUS(status, result);
			goto end;
		} // if ( SCARD_S_SUCCESS != result)
		offset += responseDataLength - 2;
		// main switch block for cases 2 and 4

		switch (caseAPDU) {
		case 2: {

			while ((responseData[offset] == 0x61)
				|| (responseData[offset] == 0x6c)) {

					//Le is not accepted by the card and the card indicates the available length La.
					//The response TPDU from the card indicates that the command is aborted due to
					//a wrong length and that the right length is La: (SW1='6C' and SW2 codes La).

					if (responseData[offset] == 0x6c) {

						la = responseData[offset + 1];

						capdu[capduLength-1] = la; // P3

						// T=0 transmission (command w/ La)

						responseDataLength = *rapduLength - offset;
						result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
							SCARD_PCI_T0,
							capdu,
							capduLength,
							NULL,
							responseData+offset,
							&responseDataLength
							);
						if ( SCARD_S_SUCCESS != result) {
							HANDLE_STATUS(status, result);
							goto end;
						}

						// If La is greater that Le, then the response TPDU is mapped
						// onto the response APDU by keeping only the first Le bytes
						// of the body and the status bytes SW1-SW2.

						if (convert_byte(le) < convert_byte(la)) {
							memmove(responseData+offset+convert_byte(le), responseData+offset+responseDataLength-2, 2);
							offset += convert_byte(le);
							break;
						} // if (convert_byte(le) < convert_byte(la))
						offset += responseDataLength - 2;
						le = la;
						continue;

					} // if (6C)

					// Java Card specific. Java Card RE Specification Chapter 9

					// if (61)

					if (responseData[offset] == 0x61) {

						// Lr < Le
						// 1. The card sends <0x61,Lr> completion status bytes
						// 2. The CAD sends GET RESPONSE command with Le = Lr.
						// 3. The card sends Lr bytes of output data using the standard T=0 <INS> or <~INS>
						// procedure byte mechanism.
						// 4. The card sends <SW1,SW2> completion status on completion of the
						// Applet.process method.

						// Lr > Le
						// 1. The card sends Le bytes of output data using the standard T=0 <INS> or
						// <~INS> procedure byte mechanism.
						// 2. The card sends <0x61,(Lr-Le)> completion status bytes
						// 3. The CAD sends GET RESPONSE command with new Le <= Lr.
						// 4. The card sends (new) Le bytes of output data using the standard T=0 <INS> or
						// <~INS> procedure byte mechanism.
						// 5. Repeat steps 2-4 as necessary to send the remaining output data bytes (Lr) as
						// required.
						// 6. The card sends <SW1,SW2> completion status on completion of the
						// Applet.process method.

						// These two cases behave the same way
						la = responseData[offset + 1];

						capdu[0] = 0x00;
						capdu[1] = 0xC0; // INS (Get Response)
						capdu[2] = 0x00; // P1
						capdu[3] = 0x00; // P2
						capdu[4] = la;
						capduLength = 5;
						le = la;
						// T=0 transmission (command w/ La)

						responseDataLength = *rapduLength - offset;
						result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
							SCARD_PCI_T0,
							capdu,
							capduLength,
							NULL,
							responseData+offset,
							&responseDataLength
							);
						if ( SCARD_S_SUCCESS != result) {
							HANDLE_STATUS(status, result);
							goto end;
						} // if ( SCARD_S_SUCCESS != result)
						offset += responseDataLength - 2;
						continue;
					} // if (61)
			} // while (61) || (6c)
			break;
				} // case 2

		case 4: {

			/* Note: Some smartcard are not fully compatible
			with ISO normatives in case short 4.
			So when a card returns a 0x9000 to inform a good
			transfer of the APDU command then the terminal
			have to terminate the transaction and it shall
			return the sw1 sw2 to the user. In the case of
			fully ISO, the terminal sends a get response
			to extract the "le" bytes requested inside
			the APDU command. */

			// if 61 etc.

			if (responseData[offset] == 0x61
				|| responseData[offset] == 0x90
				|| responseData[offset] == 0x9F) {

					capdu[0] = 0x00;
					capdu[1] = 0xC0; // INS (Get Response)
					capdu[2] = 0x00; // P1
					capdu[3] = 0x00; // P2
					capduLength = 5;

					// Default Case Le is requested in the get response

					capdu[4] = le; // P3

					// verify if we have La < Le in the case of sw2 = 0x61 or 0x9f

					if (responseData[offset] != 0x90) {
						if (convert_byte(responseData[offset + 1]) < convert_byte(le)) {
							// La is requested in the get response

							capdu[4] = responseData[offset + 1];
							le = responseData[offset + 1];
						}
					}

					// T=0 transmission (command w/ Le or La)

					responseDataLength = *rapduLength - offset;

					// copy the data to an intermediate buffer to restore it in case of a broken ISO implementation not supporting GET RESPONSE on 0x9000
					memcpy(rapdu, responseData, offset + 2);
					tempDataLength = offset + 2;

					result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
						SCARD_PCI_T0,
						capdu,
						capduLength,
						NULL,
						responseData+offset,
						&responseDataLength
						);
					if ( SCARD_S_SUCCESS != result) {
						HANDLE_STATUS(status, result);
						goto end;
					} // if ( SCARD_S_SUCCESS != result)
					result = get_short(responseData+offset, responseDataLength-2);
					// if the result is 0x6E00 then this should be a broken ISO implementation not supporting GET RESPONSE on 0x9000 and we return the previous response data
					if (result == 0x6E00 || result == 0x6D00) {
						memcpy(responseData, rapdu, tempDataLength);
						responseDataLength = tempDataLength;
					}
					else {
						offset += responseDataLength - 2;
				    }
			} // if (61 etc.)

			while ((responseData[offset] == 0x61)
				|| (responseData[offset] == 0x6c)
				|| (responseData[offset] == 0x9f)) {

					// if (6c)

					if (responseData[offset ] == 0x6c) {

						la = responseData[offset + 1];
						capdu[capduLength -1] = la; // P3

						// T=0 transmition (command w/ La)

						responseDataLength = *rapduLength - offset;
						result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
							SCARD_PCI_T0,
							capdu,
							capduLength,
							NULL,
							responseData+offset,
							&responseDataLength
							);
						if ( SCARD_S_SUCCESS != result) {
							HANDLE_STATUS(status, result);
							goto end;
						}

						// If La is greater that Le, then the response TPDU is mapped
						// onto the response APDU by keeping only the first Le bytes
						// of the body and the status bytes SW1-SW2.

						if (convert_byte(le) < convert_byte(la)) {
							memmove(responseData+offset+convert_byte(le), responseData+offset+responseDataLength-2, 2);
							offset += convert_byte(le);
							break;
						} // if (convert_byte(le) < convert_byte(la))
						offset += responseDataLength - 2;
						le = la;
						continue;

					} // if (6c)

					// if (61) || (9f)

					if ((responseData[offset] == 0x61)
						|| (responseData[offset] == 0x9f)) {

							// Lr < Le
							// 1. The card sends <0x61,Lr> completion status bytes
							// 2. The CAD sends GET RESPONSE command with Le = Lr.
							// 3. The card sends Lr bytes of output data using the standard T=0 <INS> or <~INS>
							// procedure byte mechanism.
							// 4. The card sends <SW1,SW2> completion status on completion of the
							// Applet.process method.

							// Lr > Le
							// 1. The card sends Le bytes of output data using the standard T=0 <INS> or
							// <~INS> procedure byte mechanism.
							// 2. The card sends <0x61,(Lr-Le)> completion status bytes
							// 3. The CAD sends GET RESPONSE command with new Le <= Lr.
							// 4. The card sends (new) Le bytes of output data using the standard T=0 <INS> or
							// <~INS> procedure byte mechanism.
							// 5. Repeat steps 2-4 as necessary to send the remaining output data bytes (Lr) as
							// required.
							// 6. The card sends <SW1,SW2> completion status on completion of the
							// Applet.process method.

							la = responseData[offset + 1];

							capdu[0] = 0x00;
							capdu[1] = 0xC0; // INS (Get Response)
							capdu[2] = 0x00; // P1
							capdu[3] = 0x00; // P2
							capdu[4] = la;
							capduLength = 5;
							le = la;
							// T=0 transmission (command w/ La)

							responseDataLength = *rapduLength - offset;
							result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
								SCARD_PCI_T0,
								capdu,
								capduLength,
								NULL,
								responseData+offset,
								&responseDataLength
								);
							if ( SCARD_S_SUCCESS != result) {
								HANDLE_STATUS(status, result);
								goto end;
							}
							offset += responseDataLength - 2;
							continue;

					} // if (61) || (9f)

			} // while (61) || (6c) || (9f)

			break;
				} // case 4

		} // switch (main switch block for cases 2 and 4)

	} // if (if T=1 or else T=0)

	// if the case 3 command is actually a case 4 command and the cards wants to respond something.
	// Keep draining SW1=61 chains until final status.
	while (responseData[offset] == 0x61) {
		if (offset + 2 > *rapduLength) {
			HANDLE_STATUS(status, OPGP_ERROR_INSUFFICIENT_BUFFER);
			goto end;
		}

		la = responseData[offset + 1];

		capdu[0] = 0x00;
		capdu[1] = 0xC0; // INS (Get Response)
		capdu[2] = 0x00; // P1
		capdu[3] = 0x00; // P2
		capdu[4] = la;
		capduLength = 5;
		le = la;

		responseDataLength = *rapduLength - offset;
		result = SCardTransmit(GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->cardHandle,
			GET_PCSC_CARD_INFO_SPECIFIC(cardInfo)->protocol == OPGP_CARD_PROTOCOL_T0 ? SCARD_PCI_T0 : SCARD_PCI_T1,
			capdu,
			capduLength,
			NULL,
			responseData+offset,
			&responseDataLength
			);
		if ( SCARD_S_SUCCESS != result) {
			HANDLE_STATUS(status, result);
			goto end;
		}
		if (responseDataLength < 2) {
			HANDLE_STATUS(status, OPGP_ERROR_INVALID_RESPONSE_DATA);
			goto end;
		}
		offset += responseDataLength - 2;
	} // while (61)

	memcpy(rapdu, responseData, offset + 2);
	*rapduLength = offset + 2;

	// get SW
	result = get_short(rapdu, *rapduLength-2);

	OPGP_ERROR_CREATE_NO_ERROR_WITH_CODE(status, OPGP_ISO7816_ERROR_PREFIX | result, OPGP_stringify_error(OPGP_ISO7816_ERROR_PREFIX | result));
end:
	if (responseData) {
		free(responseData);
	}
	OPGP_LOG_END(_T("OPGP_PL_send_APDU"), status);
	return status;
}

/**
* \param errorCode [in] The error code.
* \return OPGP_STRING representation of the error code.
*/
OPGP_STRING OPGP_PL_stringify_error(DWORD errorCode) {
	if (errorCode == OPGP_PL_ERROR_NO_CARD_CONTEXT_INITIALIZED) {
		return (OPGP_STRING)_T("PC/SC plugin is not initialized. A card context must be established first.");
	}
	if (errorCode == OPGP_PL_ERROR_NO_CARD_INFO_INITIALIZED) {
		return (OPGP_STRING)_T("PC/SC plugin is not initialized. A card connection must be created first.");
	}
#ifndef WIN32
	if ((errorCode & ((DWORD)0xFFF00000L)) == ((DWORD)0x80100000L)) {
		return (OPGP_STRING)pcsc_stringify_error((long)(errorCode & 0xFFFFFFFF));
	}
#endif
	// delegate to general stringify function
	return OPGP_stringify_error(errorCode);
}
