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▲
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 !