Custom IO Device

From Qt Wiki
Jump to: navigation, search


English | German


Writing a Custom I/O Device

This is a port of the article in Qt Quarterly 12 about writing a custom QIODevice

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:

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

And on the possible usage (in our example code in git [1])


Encryption

QByteArray dataArray;

QBuffer bufferUsedLikeAFile(&dataArray);

CryptDevice deviceFilter(&bufferUsedLikeAFile);
deviceFilter.open(QIODevice::WriteOnly);
QTextStream stream(&deviceFilter);
QString szText = rawText->toPlainText();
stream << szText;


Decryption

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

Example image of the test app:

CustomIoDevice.jpg

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:

class CryptDevice : public QIODevice {

Q_OBJECT

public:

CryptDevice(QIODevice* deviceToUse, QObject* parent = 0);
bool open(OpenMode mode);
void close();
bool isSequential() const;

protected:

qint64 readData(char* data, qint64 maxSize);
qint64 writeData(const char* data, qint64 maxSize);

private:

QIODevice* underlyingDevice;
Q_DISABLE_COPY(CryptDevice)

};

The constructor definition is pretty straightforward

CryptDevice::CryptDevice(QIODevice* deviceToUse, QObject* parent) :

QIODevice(parent),
underlyingDevice(deviceToUse)

{ }

As our device should be sequential, we re-implement isSequential bool CryptDevice::isSequential() const {

return true;

}

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

bool CryptDevice::open(OpenMode mode) {

bool underlyingOk;
if (underlyingDevice->isOpen())
underlyingOk = (underlyingDevice->openMode() != mode);
else
underlyingOk = underlyingDevice->open(mode);

if (underlyingOk)

{
setOpenMode(mode);
return true;
}
return false;

}

Closing is trivial.

void CryptDevice::close() {

underlyingDevice->close();
setOpenMode(NotOpen);

}

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 0x5E.

qint64 CryptDevice::readData(char* data, qint64 maxSize) {

qint64 deviceRead = underlyingDevice->read(data, maxSize);
if (deviceRead == –1)
return -1;
for (qint64 i = 0; i < deviceRead; +i)
data[i] = data[i] ^ 0x5E;
return deviceRead;

}

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.

qint64 CryptDevice::writeData(const char* data, qint64 maxSize) {

QByteArray buffer((int)maxSize, 0);
for (int i = 0; i < (int)maxSize;+i)
buffer[i] = data[i] ^ 0x5E;
return underlyingDevice->write(buffer.data(), maxSize);

}