Problema con plugin QODBC e QString

Problema con plugin QODBC e QString

Messaggioda mondinmr » 04 feb , 2012 11:36 am

Buongiorno! Prima di tutto ne approfitto per presentarmi, non avendo trovato la sezione presentazioni su questo forum.
Non sono un programmatore professionista, ma nella vita mi occupo di gestire un azienda. Da anni ho coltivato comunque la passione per la programmazione in generale, iniziando da giovanissimo con il Basic, per poi passare al Pascal ed in fine prima al C poi al C++, i quali sono diventati i linguaggi da me preferiti negli ultimi 20 anni.
Premetto che trovo stupefacenti le possibilità che aprono le libreire QT, e le semplificazioni a cui portano per sviluppare piccoli applicativi aziendali.

Qualche anno fa ho iniziato a sviluppare diversi applicativi di appoggio per l'azienda, tali applicativi gestiscono varie funzione come l'impaginazione di documenti (Che su iSeries è pessima) etc.

Dovendo accedere a un Database presente su iSeries, ho utilizzato l'iSeriesAccess per linux rilasciato da IBM, il quale comprende il driver ODBC per l'accesso al database.

Fino a quando le varie distribuzioni linux si sono appoggiate ad iodbc non ho mai avuto problemi. I problemi sono iniziati da quando le principali distibuzioni sono passate ad unixODBC.

Accedendo con qualunque applicativo QT4 ad un database su iSeries, nel momento in cui si tenta di accedere ad un campo contenente una stringa si ottiene il seguente errore:

Codice: Seleziona tutto
gconv.c: 75: __gconv: Assertion "outbuf! = ((void *) 0) & & * outbuf! = ((void *) 0) 'failed.
Aborted


Dopo avere segnalato il BUG qua https://bugs.launchpad.net/ubuntu/+source/qt4-x11/+bug/665188 e qua https://bugreports.qt-project.org/browse/QTBUG-23675, ho deciso di iniziare a cercare la causa del problema.

Dopo mille peripezzie sono arrivato a capire dove si originava il BUG.

Come riferimento ai numeri di riga prendo in esame le librerie QT 4.7.4, anche se il problema si presenta in tutte le versioni precedenti.

Nel file src/sql/drivers/odbc/qsql_odbc.cpp dalla linea 359 si trova il seguente codice:
Codice: Seleziona tutto
static QString qGetStringData(SQLHANDLE hStmt, int column, int colSize, bool unicode = false)
{
    QString fieldVal;
    SQLRETURN r = SQL_ERROR;
    QSQLLEN lengthIndicator = 0;

    // NB! colSize must be a multiple of 2 for unicode enabled DBs
    if (colSize <= 0) {
        colSize = 256;
    } else if (colSize > 65536) { // limit buffer size to 64 KB
        colSize = 65536;
    } else {
        colSize++; // make sure there is room for more than the 0 termination
    }
    if(unicode) {
        r = SQLGetData(hStmt,
                        column+1,
                        SQL_C_TCHAR,
                        NULL,
                        0,
                        &lengthIndicator);
        if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0)
            colSize = lengthIndicator/sizeof(SQLTCHAR) + 1;
        QVarLengthArray<SQLTCHAR> buf(colSize);
        memset(buf.data(), 0, colSize*sizeof(SQLTCHAR));
        while (true) {
            r = SQLGetData(hStmt,
                            column+1,
                            SQL_C_TCHAR,
                            (SQLPOINTER)buf.data(),
                            colSize*sizeof(SQLTCHAR),
                            &lengthIndicator);
            if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) {
                if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) {
                    fieldVal.clear();
                    break;
                }
                // if SQL_SUCCESS_WITH_INFO is returned, indicating that
                // more data can be fetched, the length indicator does NOT
                // contain the number of bytes returned - it contains the
                // total number of bytes that CAN be fetched
                // colSize-1: remove 0 termination when there is more data to fetch
                int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator/sizeof(SQLTCHAR);
                    fieldVal += fromSQLTCHAR(buf, rSize);
                if ((unsigned)lengthIndicator < colSize*sizeof(SQLTCHAR)) {
                    // workaround for Drivermanagers that don't return SQL_NO_DATA
                    break;
                }
            } else if (r == SQL_NO_DATA) {
                break;
            } else {
                qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')';
                fieldVal.clear();
                break;
            }
        }
    } else {
        r = SQLGetData(hStmt,
                        column+1,
                        SQL_C_CHAR,
                        NULL,
                        0,
                        &lengthIndicator);
        if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0)
            colSize = lengthIndicator + 1;
        QVarLengthArray<SQLCHAR> buf(colSize);
        while (true) {
            r = SQLGetData(hStmt,
                            column+1,
                            SQL_C_CHAR,
                            (SQLPOINTER)buf.data(),
                            colSize,
                            &lengthIndicator);
            if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) {
                if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) {
                    fieldVal.clear();
                    break;
                }
                // if SQL_SUCCESS_WITH_INFO is returned, indicating that
                // more data can be fetched, the length indicator does NOT
                // contain the number of bytes returned - it contains the
                // total number of bytes that CAN be fetched
                // colSize-1: remove 0 termination when there is more data to fetch
                int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator;
                    fieldVal += QString::fromUtf8((const char *)buf.constData(), rSize);
                if (lengthIndicator < (unsigned int)colSize) {
                    // workaround for Drivermanagers that don't return SQL_NO_DATA
                    break;
                }
            } else if (r == SQL_NO_DATA) {
                break;
            } else {
                qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')';
                fieldVal.clear();
                break;
            }
        }
    }
    return fieldVal;
}


Il problema sorge nella chiamata al metodo SQLGetData(hStmt, column+1, SQL_C_TCHAR, NULL, 0, &lengthIndicator);

In particolare con l'indirizzo del buffer NULL, il quale probabilmente non viene accettato correttamente da iconv.
Questa chiamata da quel poco che ho analizzato credo che serva a capire quale lunghezza effettiva ha il campo, in quanto nelle righe successive viene adattato colSize al valore lengthIndicator impostato dal metodo. Successivamente viene creato un buffer adatto a contenere la stringa e viene copiato il campo in tale buffer.

Modificando il codice come segue :
Codice: Seleziona tutto
static QString qGetStringData(SQLHANDLE hStmt, int column, int colSize, bool unicode = false)
{
    QString fieldVal;
    SQLRETURN r = SQL_ERROR;
    QSQLLEN lengthIndicator = 0;

    // NB! colSize must be a multiple of 2 for unicode enabled DBs
    if (colSize <= 0) {
        colSize = 256;
    } else if (colSize > 65536) { // limit buffer size to 64 KB
        colSize = 65536;
    } else {
        colSize++; // make sure there is room for more than the 0 termination
    }
    if(unicode) {
        QVarLengthArray<SQLTCHAR> buftmp(colSize);
        memset(buftmp.data(), 0, colSize*sizeof(SQLTCHAR));       
        r = SQLGetData(hStmt,
                        column+1,
                        SQL_C_TCHAR,
                        (SQLPOINTER)buftmp.data(),
         0,
                        &lengthIndicator);
        if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0)
            colSize = lengthIndicator/sizeof(SQLTCHAR) + 1;
        QVarLengthArray<SQLTCHAR> buf(colSize);
        memset(buf.data(), 0, colSize*sizeof(SQLTCHAR));
        while (true) {
            r = SQLGetData(hStmt,
                            column+1,
                            SQL_C_TCHAR,
                            (SQLPOINTER)buf.data(),
                            colSize*sizeof(SQLTCHAR),
                            &lengthIndicator);
            if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) {
                if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) {
                    fieldVal.clear();
                    break;
                }
                // if SQL_SUCCESS_WITH_INFO is returned, indicating that
                // more data can be fetched, the length indicator does NOT
                // contain the number of bytes returned - it contains the
                // total number of bytes that CAN be fetched
                // colSize-1: remove 0 termination when there is more data to fetch
                int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator/sizeof(SQLTCHAR);
                    fieldVal += fromSQLTCHAR(buf, rSize);
                if ((unsigned)lengthIndicator < colSize*sizeof(SQLTCHAR)) {
                    // workaround for Drivermanagers that don't return SQL_NO_DATA
                    break;
                }
            } else if (r == SQL_NO_DATA) {
                break;
            } else {
                qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')';
                fieldVal.clear();
                break;
            }
        }
    } else {
        QVarLengthArray<SQLTCHAR> buftmp(colSize);
        memset(buftmp.data(), 0, colSize*sizeof(SQLCHAR));
        r = SQLGetData(hStmt,
                        column+1,
                        SQL_C_CHAR,
                        (SQLPOINTER)buftmp.data(),
                        0,
                        &lengthIndicator);
        if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0)
            colSize = lengthIndicator + 1;
        QVarLengthArray<SQLCHAR> buf(colSize);
        while (true) {
            r = SQLGetData(hStmt,
                            column+1,
                            SQL_C_CHAR,
                            (SQLPOINTER)buf.data(),
                            colSize,
                            &lengthIndicator);
            if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) {
                if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) {
                    fieldVal.clear();
                    break;
                }
                // if SQL_SUCCESS_WITH_INFO is returned, indicating that
                // more data can be fetched, the length indicator does NOT
                // contain the number of bytes returned - it contains the
                // total number of bytes that CAN be fetched
                // colSize-1: remove 0 termination when there is more data to fetch
                int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator;
                    fieldVal += QString::fromUtf8((const char *)buf.constData(), rSize);
                if (lengthIndicator < (unsigned int)colSize) {
                    // workaround for Drivermanagers that don't return SQL_NO_DATA
                    break;
                }
            } else if (r == SQL_NO_DATA) {
                break;
            } else {
                qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')';
                fieldVal.clear();
                break;
            }
        }
    }
    return fieldVal;
}

Ricompilando il plugin odbc e testando gli applicativi da me creati o applicativi come "tora" non creato da me. Tutto funziona regolarmente.

Ora però mi sorge un dubbio!
Quello che ho fatto io è creare un buffer temporaneo e sostituire il suo indirizzo all'indirizzo NULL che da problemi.
Lo ho creato delle dimensioni del colSize moltiplicate per la dimensione del carattere, ma ho paura che in tal modo si possa incappare in un buffer overflow, in quanto se tale metodo era stato utilizzato per misurare l'effettiva dimensione prima di creare il buffer era proprio perchè colSize moltiplicato per la lunghezza del carattere avrebbe potuto essere minore di lengthIndicator.

Con questa correzione per ora funziona, ma non credo sia una soluzione poco pulita.

Chiedo a voi se avete idee migliori e se c'è un modo per contattare chi mantiene il file src/sql/drivers/odbc/qsql_odbc.cpp per segalare il problema. Purtroppo credo che altrimenti tramite i bug report possa passare molto tempo prima che venga realizzata un patch.
mondinmr
Leggo soltanto
 
Messaggi: 3
Iscritto il: 04 feb , 2012 10:10 am
Programmo in: C, C++, Pascal, Basic

Re: Problema con plugin QODBC e QString

Messaggioda TrueNeo » 06 apr , 2012 11:13 am

Dovresti riportare il bug direttamente su qt-project.org, però prima fai una verifica con le qt 4.8.1 e la versione di ODBC che hai sul tuo sistema.
Daniele

Prima di porre domande leggere il Regolamento e FAQ, poi usare la funzione "Cerca".
Avatar utente
TrueNeo
Admin
 
Messaggi: 453
Iscritto il: 29 dic , 2009 7:42 pm
Località: Bari
Programmo in: C++

Re: Problema con plugin QODBC e QString

Messaggioda mondinmr » 02 mag , 2012 10:47 pm

Così ho fatto!
Mi sono registrato su bugreports.qt-project.org ed ho riportato il BUG. https://bugreports.qt-project.org/browse/QTBUG-24100
Alla fine ho trovato il bug nei sorgenti delle QT ed ho risolto il problema in maniera più elegante di quella sopra citata! Mi sono Messo in comunicazione con Honglei Zhang, il manteiner della parte sql, il quale mi ha ringraziato di avere trovato il BUG e mi ha invitato ad iscrivermi su gitorous e gerrit per contribuire. Ha alzato la priorità del BUG a P2.

Su gitorious mi sono registrato usando le varie chiavi SSH & C. ed ho clonato il repository ufficiale su cui ho creato una patch per la versione delle QT 4.8, poi mi sono registrato su gerrit, dove Honglei Zhang mi ha chiesto di postare tale patch, in modo che venisse valutata dal team di sviluppo. Il problema è che non sono riuscito a capire molto bene il meccanismo di gitorious e gerrit! Appena avrò un attimo approfondirò! Il principale scoglio è che tutte le spiegazioni di tali portali sono in inglese e ci metto tempo a tradurre.


Si trattava di un BUG nella lettura dei caratteri UNICODE. Il problema è sorto su varie distro linux quando da iODBC sono passate ad unixodbc. Quest'ultimo non accetta chiamate a sqlGetData(...) con puntatore al buffer di destinazione NULL e porta all'errore sopra citato. (con iODBC restituiva la dimensione del buffer necessaria a leggere i dati)

Tale chiamata era usata per dimensionare il buffer su cui portare poi i dati letti con una seconda chiamata alla stessa funzione. (Il che creava anche un notevole dispendio di tempo e risorse di rete)

Ho ovviato al problema sostituendo il dimensionamento del buffer, con una funzione statica aggiunta che calcola lo spazio necessario nel qual caso tutti i caratteri della colonna usassero la massima dimensione consentita dai caratteri unicode multibyte. Ovviamente tale buffer viene poi ridimensionato alla lunghezza più corretta dopo l'effettiva lettura dei dati, prima di essere passato ad un oggetto QString.

Ho anche modificato altre 2-3 cosette che in alcuni casi creavano problemi di memory leak e buffer overflow che causavano sporadici crash in applicativi un po' grossi, come un gestore da me creato di stampe/email/pdf per fatturazione di fine mese che si connette ad un database su iSeries (Un sostituto dei vari software carissimi per iSeries per la gestione delle stampe). I buffer overflow erano causati dal fatto che la dimensione dei caratteri UTF8 non veniva calcolata in modo corretto ed alcuni buffer ne risultavano sottodimensionati.

Dopo la pulizia, il plugin SQL ne ha guadagnato anche in velocità, riducendo del 50% i tempi necessari a leggere l'intera mole di dati dal server iSeries.

Su windows usando carateri unicode a lunghezza fissa probabilmente funziona tutto, anche perché la funzione sqlGetData(...) "dovrebbe" svolgere il lavoro correttamente alla prima chiamata con puntatore NULL, ma con la patch se ne guadagna comunque notevolmente in velocità, il ché non è mai un male quando si deve accedere a database leggendo grosse moli di dati.

Alla pagina che ho indicato sopra ho postato il sorgente modificato per le QT4.7, mentre su gitorious al repository clone mondinmr, si trova quello per le QT 4.8, che poi funziona benissimo anche compilato sulle 4.7.
mondinmr
Leggo soltanto
 
Messaggi: 3
Iscritto il: 04 feb , 2012 10:10 am
Programmo in: C, C++, Pascal, Basic

Re: Problema con plugin QODBC e QString

Messaggioda TrueNeo » 05 mag , 2012 5:32 pm

Ottimo lavoro.
Daniele

Prima di porre domande leggere il Regolamento e FAQ, poi usare la funzione "Cerca".
Avatar utente
TrueNeo
Admin
 
Messaggi: 453
Iscritto il: 29 dic , 2009 7:42 pm
Località: Bari
Programmo in: C++

Re: Problema con plugin QODBC e QString

Messaggioda mondinmr » 09 mag , 2012 10:11 am

Finalmente sto iniziando a capire i meccanismi di git e gerrit e sono riuscito a postare la patch passando il sanity check!

https://codereview.qt-project.org/#change,25689
mondinmr
Leggo soltanto
 
Messaggi: 3
Iscritto il: 04 feb , 2012 10:10 am
Programmo in: C, C++, Pascal, Basic


Torna a Qt & C++

Chi c’è in linea

Visitano il forum: Nessuno e 0 ospiti

cron