Writing a Custom I/O Device

Материал из Wiki.crossplatform.ru

(Различия между версиями)
Перейти к: навигация, поиск
(Новая: {{menu_Qt_Издания}} by Morten Sшrvig <blockquote>'''In this article we'll develop a QIODevice subclass that encrypts anddecrypts a data stream on the fly. The class acts as a wr...)
 
Строка 1: Строка 1:
-
{{menu_Qt_Издания}}
+
{{Панель навигации по Qt Quarterly|Выпуск 12}}
by Morten Sшrvig
by Morten Sшrvig

Текущая версия на 03:56, 20 февраля 2009

Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 12 | Документация


by Morten Sшrvig

In this article we'll develop a QIODevice subclass that encrypts anddecrypts a data stream on the fly. The class acts as a wrapper foran I/O device, such as QFile and QSocket, and can be combined withQTextStream or QDataStream. Finally, we'll see what API improvementsthe Qt 4 QIODevice has to offer.

[Download Source Code]

The following code snippet shows how we would use the custom I/Odevice to encrypt data and store the result in a file:

QFile file("output.dat");
CryptDevice cryptDevice(&amp;file)
QTextStream out(&amp;cryptDevice);
cryptDevice.open(QIODevice::WriteOnly);
out << "Hello World";

The data written to the QTextStream goes through theCryptDevice before it reaches the QFile. Similarly, when QTextStream tries to read data, CryptDevice stands between itand the QFile device.

The advantage of using an I/O stream subclass, as opposed toperforming the encryption or decryption in a separate pass, is thatit doesn't require the entire file to be in memory and it spreads outthe encrypting computations in time.

For this example, we'll use a trivial XOR-based encoding scheme. Thiswill of course give rather weak security, so in a real application aproper encryption scheme should be used. The point of this article isnot so much to do encryption as to show how to subclassQIODevice.

[править] The Custom I/O Device

Writing a custom QIODevice class in Qt 3 involves inheritingQIODevice and then reimplementing a set of virtual functions.

One issue when writing a wrapper around an existing QIODevice isthat the device can be either synchronous or asynchronous. Synchronousdevices read and write data immediately, while asynchronous devices may notdeliver data until seconds later. QIODevices are also divided into directaccess and sequential access devices. Direct access devices (for example, QFile)offer random-access seeking, while sequental access devices (for example, QSocket)only support streaming data.

Our CryptDevice class will be a sequential I/O device. Whetherit's synchronous or asynchronous depends on the underlyingQIODevice.

[править] Source Code

The class definition looks like this:

class CryptDevice : public QIODevice
{
public:
    CryptDevice(QIODevice *underlyingDevice);
 
    bool open(int mode);
    void close();
    void flush();
    Offset at() const;
    bool at(int pos) const;
    Offset size() const;
    Q_LONG readBlock(char *data, Q_ULONG maxSize);
    Q_LONG writeBlock(const char *data, Q_ULONG size);
    int getch();
    int putch(int ch);
    int ungetch(int ch);
 
private:
    QIODevice *underlyingDevice;
    QValueStack<char> ungetchBuffer;
};

The public functions are all reimplemented from QIODevice.

CryptDevice::CryptDevice(QIODevice *underlyingDevice)
    : underlyingDevice(underlyingDevice)
{
    setType(IO_Sequential);
}

The constructor definition is pretty straightforward: We take apointer to the wrapped QIODevice as an argument and set theIO_Sequential flag to indicate that the device is sequential (asopposed to random-access).

bool CryptDevice::open(int mode)
{
    bool underlyingOk;
    if (underlyingDevice->isOpen()) 
        underlyingOk = (underlyingDevice->mode() != mode);
    else 
        underlyingOk = underlyingDevice->open(mode);
 
    if (underlyingOk) {
        setState(IO_Open);
        return true;
    }
    return false;
}

In open(), we open the underlying device if it's not already openand set the device state to IO_Open.

void CryptDevice::close()
{
    underlyingDevice->close();
    setState(0);
}
 
void CryptDevice::flush()
{
    underlyingDevice->flush();
}

Closing and flushing are trivial.

int CryptDevice::getch()
{
    char ch;
    if (readBlock(&amp;ch, 1) == 1)
        return (uchar)ch;
    else
        return -1;
}
 
int CryptDevice::putch(int ch)
{
    char data = (char)ch;
    if (writeBlock(&amp;data, 1) == 1)
        return ch;
    else
        return -1;
}

The getch() and putch() functions are based onreadBlock(), which we will review in a moment.

int CryptDevice::ungetch(int ch)
{
    ungetchBuffer.push((char)ch);
    return ch;
}

The ungetch() function puts one character back onto the device,canceling the last getch(). In theory, we could simply callungetch() on the underlying device because our encrypting schemeis trivial (one character in the underlying device corresponds to onecharacter in the CryptDevice); however, to show how to implementan ungetch() buffer, we'll roll our own.

Q_LONG CryptDevice::readBlock(char *data, Q_ULONG maxSize)
{
    Q_ULONG ungetchRead = 0;
    while (!ungetchBuffer.isEmpty()
           &amp;&amp; ungetchRead < maxSize)
        data[ungetchRead++] = ungetchBuffer.pop();
 
    Q_LONG deviceRead = underlyingDevice->readBlock(data +
                      ungetchRead, maxSize - ungetchRead);
    if (deviceRead == -1)
        return -1;
    for (Q_LONG i = 0; i < deviceRead; ++i)
        data[i] = data[ungetchRead + i] ^ 0x5E;
 
    return ungetchRead + deviceRead;
}

When reading a block, we start by reading any "ungotten"characters, then we call readBlock() on the underlying device. Atthe end, we XOR each byte read from the device with the magicconstant 0x5E.

Q_LONG CryptDevice::writeBlock(const char *data, Q_ULONG size)
{
    QByteArray buffer(size);
    for (Q_ULONG i = 0; i < size; ++i)
        buffer[i] = data[i] ^ 0x5E;
    return underlyingDevice->writeBlock(buffer.data(),
                                        size);
}

When writing a block, we create a temporary buffer with the XOR'ddata. A more efficient implementation would use a 4096-byte buffer onthe stack and call writeBlock() multiple times if size islarger than the buffer.

QIODevice::Offset CryptDevice::at() const
{
    return 0;
}

The at() function returns the current device position. Forsequential devices, it should always return 0.

bool CryptDevice::at(Offset /* offset */)
{    
    return false;
}

QIODevice has an at() overload that sets the position ofthe device. For sequential devices, this makes no sense, so we returnfalse.

QIODevice::Offset CryptDevice::size() const
{
    return underlyingDevice->size();
}

In size(), we return the size of the underlying device. This ispossible in this simple example, because of the trivial encryptionalgorithm. If the size is unknown, we could return 0.

[править] QIODevices in Qt 4

The QIODevice class in Qt 4 will differ in some aspects comparedwith the one in Qt 3. The main difference is that it inherits QObject and provides signals and slots to notify otherapplications about incoming data. This means that it will be easierto implement custom devices that support asynchronous operation. Inaddition the API has been cleaned up, so that subclassing aQIODevice only requires reimplementing two functions:readData() and writeData().