Trolltech | Documentation | Qt Quarterly | « Qt 4's New Style of Iterators

Writing a Custom I/O Device
by Morten Sørvig
In this article we'll develop a QIODevice subclass that encrypts and decrypts a data stream on the fly. The class acts as a wrapper for an I/O device, such as QFile and QSocket, and can be combined with QTextStream or QDataStream. Finally, we'll see what API improvements the Qt 4 QIODevice has to offer.

[Download Source Code]

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

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

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

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

For this example, we'll use a trivial XOR-based encoding scheme. This will of course give rather weak security, so in a real application a proper encryption scheme should be used. The point of this article is not so much to do encryption as to show how to subclass QIODevice.

The Custom I/O Device

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

One issue when writing a wrapper around an existing QIODevice is that the device can be either synchronous or asynchronous. Synchronous devices read and write data immediately, while asynchronous devices may not deliver data until seconds later. QIODevices are also divided into direct access 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. Whether it's synchronous or asynchronous depends on the underlying QIODevice.

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 a pointer to the wrapped QIODevice as an argument and set the IO_Sequential flag to indicate that the device is sequential (as opposed 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 open and 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(&ch, 1) == 1)
            return (uchar)ch;
        else
            return -1;
    }
    
    int CryptDevice::putch(int ch)
    {
        char data = (char)ch;
        if (writeBlock(&data, 1) == 1)
            return ch;
        else
            return -1;
    }
    

The getch() and putch() functions are based on readBlock(), 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 call ungetch() on the underlying device because our encrypting scheme is trivial (one character in the underlying device corresponds to one character in the CryptDevice); however, to show how to implement an ungetch() buffer, we'll roll our own.

    Q_LONG CryptDevice::readBlock(char *data, Q_ULONG maxSize)
    {
        Q_ULONG ungetchRead = 0;
        while (!ungetchBuffer.isEmpty()
               && 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. At the end, we XOR each byte read from the device with the magic constant 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'd data. A more efficient implementation would use a 4096-byte buffer on the stack and call writeBlock() multiple times if size is larger than the buffer.

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

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

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

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

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

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

QIODevices in Qt 4

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


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2005 Trolltech Trademarks Qt Quarterly