Writing a Custom I/O Device

This is a port of the article in Qt Quarterly 12 about writing a custom QIODevice [doc.qt.nokia.com]

Usage:

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

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

And on the possible usage (in our example code in git qtdevnet-wiki-mvc/qtdevnet-custom-iodevice [gitorious.org])

Encryption

  1.     QByteArray dataArray;
  2.  
  3.     QBuffer bufferUsedLikeAFile(&dataArray);
  4.     CryptDevice deviceFilter(&bufferUsedLikeAFile);
  5.     deviceFilter.open(QIODevice::WriteOnly);
  6.     QTextStream stream(&deviceFilter);
  7.     QString szText = rawText->toPlainText();
  8.     stream << szText;

Decryption

  1.     QBuffer bufferUsedLikeAFile(&dataArray);
  2.     CryptDevice deviceFilter(&bufferUsedLikeAFile);
  3.     deviceFilter.open(QIODevice::ReadOnly);
  4.     QTextStream stream(&deviceFilter);
  5.     QString szText = stream.readAll();
  6.     decryptedText->setPlainText(szText);

Example image of the test app:

Test app

The Custom I/O Device

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

There is a big difference regarding writing a custom IO device compared to Qt 3: you only have to rewrite 2 functions:

  • qint64 QIODevice::readData ( char * data, qint64 maxSize )
  • qint64 QIODevice::writeData ( const char * data, qint64 maxSize )

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:

  1. class CryptDevice : public QIODevice
  2. {
  3.     Q_OBJECT
  4. public:
  5.     CryptDevice(QIODevice* deviceToUse, QObject* parent = 0);
  6.     bool open(OpenMode mode);
  7.     void close();
  8.     bool isSequential() const;
  9. protected:
  10.     qint64 readData(char* data, qint64 maxSize);
  11.     qint64 writeData(const char* data, qint64 maxSize);
  12. private:
  13.     QIODevice* underlyingDevice;
  14.     Q_DISABLE_COPY(CryptDevice)
  15. };

The constructor definition is pretty straightforward

  1. CryptDevice::CryptDevice(QIODevice* deviceToUse, QObject* parent) :
  2.     QIODevice(parent),
  3.     underlyingDevice(deviceToUse)
  4. {
  5. }

As our device should be sequential, we re-implement isSequential

  1. bool CryptDevice::isSequential() const
  2. {
  3.     return true;
  4. }

In open(), we open the underlying device if it’s not already open and set the device state to mode.

  1. bool CryptDevice::open(OpenMode mode)
  2. {
  3.     bool underlyingOk;
  4.     if (underlyingDevice->isOpen())
  5.         underlyingOk = (underlyingDevice->openMode() != mode);
  6.     else
  7.         underlyingOk = underlyingDevice->open(mode);
  8.  
  9.     if (underlyingOk)
  10.     {
  11.         setOpenMode(mode);
  12.         return true;
  13.     }
  14.     return false;
  15. }

Closing is trivial.

  1. void CryptDevice::close()
  2. {
  3.     underlyingDevice->close();
  4.     setOpenMode(NotOpen);
  5. }

When reading a block, we call read() on the underlying device. At the end, we XOR each byte read from the device with the magic constant 0×5E.

  1. qint64 CryptDevice::readData(char* data, qint64 maxSize)
  2. {
  3.     qint64 deviceRead = underlyingDevice->read(data, maxSize);
  4.     if (deviceRead == -1)
  5.         return -1;
  6.     for (qint64 i = 0; i < deviceRead; ++i)
  7.         data[i] = data[i] ^ 0x5E;
  8.  
  9.     return deviceRead;
  10. }

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 write() multiple times if size is larger than the buffer.

  1. qint64 CryptDevice::writeData(const char* data, qint64 maxSize)
  2. {
  3.     QByteArray buffer((int)maxSize, 0);
  4.     for (int i = 0; i < (int)maxSize; ++i)
  5.         buffer[i] = data[i] ^ 0x5E;
  6.     return underlyingDevice->write(buffer.data(), maxSize);
  7. }

Categories: