Écrire un périphérique personnalisé d'E/S

| Morten Sørvig | CUVELIER Thibaut (traducteur) | Qt Quarterly |

Dans cet article, nous allons développer un dérivé de QIODevice, qui chiffre et déchiffre à la volée un flux. La classe agit comme un wrapper pour une classe d'E/S, comme QFile ou QSocket. Elle peut même être combinée avec QTextStream ou QDataStream. Finalement, nous verrons quelles améliorations QIODevice peut nous apporter.

Cet article est une traduction autorisée de Writing a Custom I/O Device par Morten Sørvig.

Commentez

- Titre : Écrire un périphérique personnalisé d'E/S

- Auteurs : Morten Sørvig, CUVELIER Thibaut (traducteur), Qt Quarterly

- Parution : 22 février 2009

- Mise à jour : 1 août 2011

- Licence : Copyright ® 2004 Morten Sørvig. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Nos buts, en guise d'introduction

- III. Le périphérique personnalisé

- IV. Le code source

- V. QIODevices dans Qt4

- VI. Divers

I - L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Writing a Custom I/O Device de Morten Sørvig paru dans la Qt Quarterly Issue 12.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II - Nos buts, en guise d'introduction

Le petit bout de code ci-après montre la manière d'utiliser notre périphérique d'E/S pour chiffrer des données et stocker le résultat dans un fichier.

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

Les données écrites dans le QTextStream passent par le CryptDevice avant d'atteindre QFile. De la même manière, quand QTextStream essaye de lire des données, CryptDevice s'interpose entre elle et QFile.

L'avantage d'utiliser une classe de flux d'E/S, à l'opposé de réaliser le chiffrement dans une passe séparée, est que le fichier ne doit pas être entièrement chargé en mémoire, ce qui améliore le temps de chiffrement.

Dans cet exemple, nous allons un chiffrement trivial à base de XOR. Ceci ne pourra pas garantir la moindre sécurité, cet exemple ne peut donc pas être utilisé dans une application réelle. Le but de cet article n'est pas tant le déchiffrement que la dérivation de QIODevice.

III - Le périphérique personnalisé

Écrire une classe QIODevice personnalisée dans Qt3 implique l'héritage à QIODevice et la réimplémentation d'une série de fonctions virtuelles

Un problème, qui apparaît, lors de l'écriture d'un wrapper, autour d'un QIODevice existant, est que le périphérique peut être, soit synchrone, soit asynchrone.
Les périphériques synchrones écrivent et lisent les données immédiatement ; les asynchrones peuvent prendre jusqu'à quelques secondes pour délivrer les données.
Les QIODevices sont aussi divisés entre périphériques à accès direct et à accès séquentiel.
Les périphériques à accès séquentiel (comme, par exemple, QSocket) offrent uniquement un streaming de données, pendant que ceux à accès direct offrent un accès aléatoire (comme QFile).

Notre classe CryptDevice va être un périphérique à accès séquentiel. Que la classe soit synchrone ou non ne dépend que du QIODevice qui l'utilise.

IV - Le code source

La définition de la classe ressemble à ceci.

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;
};

Les fonctions publiques sont toutes réimplémentées à partir de QIODevice.

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

La définition du constructeur est très rapide :
Nous prenons un pointeur au QIODevice comme argument, et utilisons le drapeau IO_Sequential pour indiquer que le périphérique est séquentiel (par opposition aux accès aléatoires).

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;
}

Dans la fonction open(), nous ouvrons le périphérique à la base, s'il ne l'est pas déjà, et nous mettons son état à IO_Open.

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

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

La fermeture et le vidage sont très simples.

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;
}

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

Les fonctions getch() et putch() sont basées sur readBlock(), que nous allons revoir dans un instant.

La fonction ungetch() met un caractère sur le périphérique, en annulant le dernier getch(). En théorie, nous aurions pu, tout simplement, appeler ungetch() sur le périphérique sous-jacent, parce que notre chiffrement est très basique (un caractère sur le périphérique correspond à un caractère dans le CryptDevice. Cependant, pour montrer comment implémenter un buffer avec ungetch(), nous implémentons différemment cette fonction.

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;
}

À la lecture d'un bloc, nous commençons par lire tous les caractères non obtenus. Ensuite, nous appelons readBlock() sur le périphérique sous-jacent. À la fin, nous XORons chaque bit lu sur le périphérique avec la constante magique 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);
}

À l'écriture d'un bloc, nous créons un buffer temporaire, rempli avec les données XORées. Une implémentation plus efficace aurait utilisé un buffer de 4069 bits sur le tas, et appelé writeBlock de nombreuses fois si la taille était plus grande que le buffer.

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

La fonction at() retourne la position actuelle du périphérique. Pour des périphériques séquentiels, elle devrait toujours retourner 0.

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

QIODevice a une surcharge de at() qui impose la position du périphérique. Pour les séquentiels, ceci n'a aucun sens.

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

Dans la fonction size(), nous retournons la taille du périphérique sous-jacent. Ceci est possible dans ce simple exemple, parce que l'algorithme de chiffrement est trivial. Si la taille est inconnue, nous pouvons retourner 0.

V - QIODevices dans Qt4

La classe QIODevice de Qt4 diffère en de nombreux aspects avec celle de Qt3. La principale est qu'elle hérite de QObject, et fournit des signaux et des slots pour notifier les autres applications des données arrivant. Ceci signifie qu'il est plus facile d'implémenter des périphériques qui supportent les opérations asynchrones. En plus, l'API a été nettoyée. Ainsi, la dérivation de QIODevice requiert uniquement deux fonctions : readData() et writeData().

VI - Divers

Le code source.

Un tout grand merci à matrix788 pour sa relecture !

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !