Game Programming Using Qt Beginner's Guide
上QQ阅读APP看书,第一时间看更新

Time for action – implementing a device to encrypt data

Let's implement a really simple device that encrypts or decrypts the data that is streamed through it using a very simple algorithm—the Caesar cipher. What it does is that when encrypting, it shifts each character in the plaintext by a number of characters defined by the key and does the reverse when decrypting. Thus, if the key is 2 and the plaintext character is a, the ciphertext becomes c. Decrypting z with the key 4 will yield the value v.

We will start by creating a new empty project and adding a class derived from QIODevice. The basic interface of the class is going to accept an integer key and set an underlying device that serves as the source or destination of data. This is all simple coding that you should already understand, so it shouldn't need any extra explanation, as shown:

class CaesarCipherDevice : public QIODevice
{
    Q_OBJECT
    Q_PROPERTY(int key READ key WRITE setKey)
public:
    explicit CaesarCipherDevice(QObject *parent = 0) : QIODevice(parent) {
      m_key = 0;
      m_device = 0;
    }
    void setBaseDevice(QIODevice *dev) { m_device = dev; }
    QIODevice *baseDevice() const { return m_device; }
    void setKey(int k) { m_key = k; }
    inline int key() const { return m_key; }
private:
    int m_key;
    QIODevice *m_device;
};

The next thing is to make sure that the device cannot be used if there is no device to operate on (that is, when m_device == 0). For this, we have to reimplement the QIODevice::open() method and return false when we want to prevent operating on our device:

bool open(OpenMode mode) {
  if(!baseDevice())
    return false;
  if(baseDevice()->openMode() != mode)
    return false;
  return QIODevice::open(mode);
}

The method accepts the mode that the user wants to open the device with. We perform an additional check to verify that the base device was opened in the same mode before calling the base class implementation that will mark the device as open.

To have a fully functional device, we still need to implement the two protected pure virtual methods, which do the actual reading and writing. These methods are called by Qt from other methods of the class when needed. Let's start with writeData(), which accepts a pointer to a buffer containing the data and size of that a buffer:

qint64 CaesarCipherDevice::writeData(const char *data, qint64 len) {
    QByteArray ba(data, len);
    for(int i=0;i<len;++i)
      ba.data()[i] += m_key;
    int written = m_device->write(ba);
    emit bytesWritten(written);
    return written;
}

First, we copy the data into a local byte array. Then, we iterate the array, adding to each byte the value of the key (which effectively performs the encryption). Finally, we try to write the byte array to the underlying device. Before informing the caller about the amount of data that was really written, we emit a signal that carries the same information.

The last method that we need to implement is the one that performs decryption by reading from the base device and adding the key to each cell of the data. This is done by implementing readData(), which accepts a pointer to the buffer that the method needs to write to and the size of the buffer. The code is quite similar to that of writeData() except that we are subtracting the key value instead of adding it:

qint64 CaesarCipherDevice::readData(char *data, qint64 maxlen) {
  QByteArray baseData = m_device->read(maxlen);
  const int s = baseData.size();
 for(int i=0;i<s;++i)
    data[i] = baseData[i]-m_key;
  return s;
}

First, we read from the underlying device as much as we can fit into the buffer and store the data in a byte array. Then, we iterate the array and set subsequent bytes of data buffer to the decrypted value. Finally, we return the amount of data that was really read.

A simple main() function that can test the class looks as follows:

int main(int argc, char **argv) {
  QByteArray ba = "plaintext";
  QBuffer buf;
  buf.open(QIODevice::WriteOnly);
  CaesarCipherDevice encrypt;
  encrypt.setKey(3);
  encrypt.setBaseDevice(&buf);
  encrypt.open(buf.openMode());
  encrypt.write(ba);
  qDebug() << buf.data();

  CaesarCipherDevice decrypt;
  decrypt.setKey(3);
  decrypt.setBaseDevice(&buf);
  buf.open(QIODevice::ReadOnly);
  decrypt.open(buf.openMode());
  qDebug() << decrypt.readAll();
  return 0;
}

We use the QBuffer class that implements the QIODevice API and acts as an adapter for QByteArray or QString.

What just happened?

We created an encryption object and set its key to 3. We also told it to use a QBuffer instance to store the processed content. After opening it for writing, we sent some data to it that gets encrypted and written to the base device. Then, we created a similar device, passing the same buffer again as the base device, but now, we open the device for reading. This means that the base device contains ciphertext. After this, we read all data from the device, which results in reading data from the buffer, decrypting it, and returning the data so that it can be written to the debug console.

Have a go hero – a GUI for the Caesar cipher

You can combine what you already know by implementing a full-blown GUI application that is able to encrypt or decrypt files using the Caesar cipher QIODevice class that we just implemented. Remember that QFile is also QIODevice, so you can pass its pointer directly to setBaseDevice().

This is just a starting point for you. The QIODevice API is quite rich and contains numerous methods that are virtual, so you can reimplement them in subclasses.

Text streams

Much of the data produced by computers nowadays is based on text. You can create such files using a mechanism that you already know—opening QFile to write, converting all data into strings using QString::arg(), optionally encoding strings using QTextCodec, and dumping the resulting bytes to the file by calling write. However, Qt provides a nice mechanism that does most of this automatically for you in a way similar to how the standard C++ iostream classes work. The QTextStream class operates on any QIODevice API in a stream-oriented way. You can send tokens to the stream using the << operator, where they get converted into strings, separated by spaces, encoded using a codec of your choice, and written to the underlying device. It also works the other way round; using the >> operator, you can stream data from a text file, transparently converting it from strings to appropriate variable types. If the conversion fails, you can discover it by inspecting the result of the status() method—if you get ReadPastEnd or ReadCorruptData, then this means that the read has failed.

Tip

While QIODevice is the main class that QTextStream operates on, it can also manipulate QString or QByteArray, which makes it useful for us to compose or parse strings.

Using QTextStream is simple—you just have to pass it the device that you want it to operate on and you're good to go. The stream accepts strings and numerical values:

QFile file("output.txt");
file.open(QFile::WriteOnly|QFile::Text);
QTextStream stream(&file);
stream << "Today is " << QDate::currentDate().toString() << endl;
QTime t = QTime::currentTime();
stream << "Current time is " << t.hour() << " h and " << t.minute() << "m." << endl;

Apart from directing content into the stream, the stream can accept a number of manipulators, such as endl, which have a direct or indirect influence on how the stream behaves. For instance, you can tell the stream to display a number as decimal and another as hexadecimal with uppercase digits using the following code (highlighted in the code are all manipulators):

for(int i=0;i<10;++i) {
  int num = qrand() % 100000;  // random number between 0 and 99999
  stream << dec << num << showbase << hex << uppercasedigits << num << endl;
}

This is not the end of the capabilities of QTextStream. It also allows us to display data in a tabular manner by defining column widths and alignments. Suppose that you have a set of records for game players that is defined by the following structure:

struct Player {
  QString name;
  qint64 experience;
  QPoint position;
  char direction;
};
QList<Player> players;

Let's dump such info into a file in a tabular manner:

QFile file("players.txt");
file.open(QFile::WriteOnly|QFile::Text);
QTextStream stream(&file);
stream << center;
stream << qSetFieldWidth(16) << "Player" << qSetFieldWidth(0) << " ";
stream << qSetFieldWidth(10) << "Experience" << qSetFieldWidth(0) << " ";
stream << qSetFieldWidth(13) << "Position" << qSetFieldWidth(0) << " ";
stream << "Direction" << endl;
for(int i=0;i<players.size();++i) {
  const Player &p = players.at(i);
  stream << left << qSetFieldWidth(16) << p.name << qSetFieldWidth(0) << " ";
  stream << right << qSetFieldWidth(10) << p.experience << qSetFieldWidth(0) << " ";
  stream << right << qSetFieldWidth(6) << p.position.x() << qSetFieldWidth(0) << " " << qSetFieldWidth(6) << p.position.y() << qSetFieldWidth(0) << " ";
  stream << center << qSetFieldWidth(10);
  switch(p.direction) {
    case 'n' : stream << "north"; break;
    case 's' : stream << "south"; break;
    case 'e' : stream << "east"; break;
    case 'w' : stream << "west"; break;
    default: stream << "unknown"; break;
  }
  stream << qSetFieldWidth(0) << endl;
}

After running the program, you should get a result similar to the one shown in the following screenshot:

One last thing about QTextStream is that it can operate on standard C file structures, which makes it possible for us to use QTextStream to, for example, write to stdout or read from stdin, as shown in the following code:

QTextStream qout(stdout);
qout << "This text goes to process standard output." << endl;

Data serialization

More than often, we have to store object data in a device-independent way so that it can be restored later, possibly on a different machine with a different data layout and so on. In computer science, this is called serialization. Qt provides several serialization mechanisms and now we will have a brief look at some of them.

Binary streams

If you look at QTextStream from a distance, you will notice that what it really does is serialize and deserialize data to a text format. Its close cousin is the QDataStream class that handles serialization and deserialization of arbitrary data to a binary format. It uses a custom data format to store and retrieve data from QIODevice in a platform-independent way. It stores enough data so that a stream written on one platform can be successfully read on a different platform.

QDataStream is used in a similar fashion as QTextStream—the operators << and >> are used to redirect data into or out of the stream. The class supports most of the built-in Qt types so that you can operate on classes such as QColor, QPoint, or QStringList directly:

QFile file("outfile.dat");
file.open(QFile::WriteOnly|QFile::Truncate);
QDataStream stream(&file);
double dbl = 3.14159265359;
QColor color = Qt::red;
QPoint point(10, -4);
QStringList stringList = QStringList() << "foo" << "bar";
stream << dbl << color << point << stringList;

If you want to serialize custom data types, you can teach QDataStream to do that by implementing proper redirection operators.