Mini-cours de C

Partie IV

Notre activité avec un ordinateur consiste à utiliser des logiciels (= programmes avec une interface graphique).

Le C est un langage de programmation qui permet de créer un programme (qui n'affiche rien (= processus lancés en arrière-plan), qui affiche quelque chose via une console (= application console) ou via une interface graphique (= logiciel))

Un programme ne sait que recevoir et/ou émettre des octets, que lire (depuis le clavier (stdin), le disque dur (local ou d'un serveur)) et/ou écrire (sur l'écran (stdout), sur le disque dur (local ou distant), ou l'imprimante) ... un flux d'octets.

Ce flux d'octets est l'échange entre le processeur et la mémoire d'un programme ou d'un périphérique. Lorsque ce périphérique est le disque dur, on parle de fichier.

Ouvrir un fichier

Pour ouvrir (en lecture, écriture ou ajout) un fichier, il faut indiquer le nom du fichier à ouvrir (avec son éventuel chemin)

Le nom d'un fichier ne doit pas nécessairement posséder une extension.

Chemin :

Ici, pour simplifier le code, le fichier se trouvera dans le dossier de l'exécutable. Donc, sans chemin.

La fonction fopen() a deux paramètres. Le premier est le nom du fichier (avec son éventuel chemin) et le second, une string qui, dans les cas simples, ne contient qu'un seul caractère.

L'ordre des caractères n'a pas d'importance. rb+ = r+b = +rb = ...

La fonction fopen() retourne un pointeur de fichier.

FILE* est le "type" de pointeur de fichier. Comme les variables, le pointeur de fichier a un nom.

Au final, on écrira : FILE* pF = fopen(nomFichier, "r");
où "pF" est le nom du pointeur de fichier et "r" le mode d'ouverture du fichier (ici, lecture).

Le mode d'ouverture indique le type de communication avec le disque dur.

Après ouverture, toute communication éventuelle avec le disque dur passera par le pointeur de fichier, appelé du nom que vous voulez, ici "pF".

La première chose à faire est de vérifier si le fichier existe (en mode lecture), si le fichier n'est pas utilisé par un autre programme et vos droits sur ce fichier. Si la fonction fopen() ne retournera pas NULL, il retourne un pointeur de fichier.

Pour ouvrir un fichier, nous avons besoin :

  1. un nom de fichier
  2. un mode d'ouverture
  3. un pointeur de fichier

Définissons deux variables globales dont leur contenu sera accessible à toute fonction. Donc, déclarons les avant toute fonction.

L'utilisation de variables globales est à éviter. Ici, cette utilisation vise à simplifier le code.

Déclarons notre fonction ouvrirFichier() ayant comme paramètre le type d'ouverture du fichier. Cette fonction modifiera la valeur du pointeur de fichier, pF (variable globale), et retournera 1 (vrai) ou 0 (faux) selon que le contenu du fichier est accessible ou non (= selon que la valeur de ce pointeur de fichier soit non-NULL ou NULL)

Comme cette fonction peut être appelée par d'autres fonctions - ecrire(), ajouter(), lire() et crypter() - déclarons avant celles-ci.

#include <stdio.h>
#include <string.h>

#define MAX 100

// variables globales ---------------------------
FILE* pF=NULL; // pF pour pointeurFichier
char nomFichier[30]; // nom du fichier
char ligne[MAX]; // contient une ligne du fichier
// fin des variables globales -------------------

int ouvrirFichier(char typeDouverture[]){

  printf("Nom du fichier TEXTE : ");
  scanf("%s",nomFichier);
  fflush(stdin);

  pF = fopen(nomFichier, typeDouverture);

  if (pF == NULL){
    printf("Impossible d'ouvrir le fichier '%s'",nomFichier);
    return 0;
  }

  return 1;
}

Une fois le fichier ouvert, il ne faut pas oublier de fermer le fichier, via la fonction fclose() qui ne prend qu'un seul paramètre : le nom d'un pointeur de fichier. Cela permet de libérer la mémoire utilisée pour la communication entre le programme et le disque dur (de vider le buffer) et, en mode enregistrement ("w" ou "a") de s'assurer que ce qui resterait dans cette mémoire volatile soit bien envoyé sur le disque dur (dans le fichier).

L'appel de la fonction fclose() sera fait dans chacune des fonctions appelant notre fonction ouvrirFichier() : ecrire(), ajouter(), lire() et crypter()

Le nom du fichier à lire doit être dans le même dossier que l'EXE.

Lire un fichier texte

Pour une lecture seule d'un fichier texte, le mode d'ouverture est "r"

Les formalités d'ouverture étant écrites, il ne reste plus qu'à écrire le code pour lire le contenu du fichier, ligne par ligne.

Pour lire plusieurs lignes, dont le nombre est inconnu, on utilise une boucle while dont le corps ne contient qu'une seule instruction : afficher la ligne lue.

La condition de sortie de la boucle while utilise la fonction fgets() qui permet de récupérer une ligne du fichier. Une ligne est une suite de caractères qui se termine par le caractère non imprimable de fin de ligne. Une longueur maximale de la ligne est fixée (au cas où le caractère de fin de ligne ne serait pas trouvé dans ces x caractères). Si la fonction trouve une ligne, elle la met dans la variable ligne, place son curseur dans le fichier à la fin de cette ligne et retourne une valeur non NULL. Les instructions dans le corps de la boucle sont alors exécutées. Si, après le curseur, la fonction ne trouve plus rien, elle retourne NULL. Elle a atteint la fin du fichier.

Enfin, à la fin de la fonction, on n'oublie pas de fermer le fichier.

Cette fonction lira aussi les fichiers binaires. Pour faire la distinction entre fichier texte et binaire, la fonction utilisera un paramètre. Si la valeur de ce paramètre (ici, mode) vaut le caractère 'c', la fonction ouvrira le fichier texte.

void lire(char mode){

  if(mode=='c'){ // affichage en caractères

    if(!ouvrirFichier("r")) return;

    printf("Rappel : N'affiche que les caractEres imprimables\n");

    while (fgets(ligne, MAX, pF) != NULL){
      printf("%s", ligne);
    }

  }else{
    // instruction pour lire un fichier binaire
  }
  fclose(pF); // pour libérer la mémoire utilisée pour la communication
}

Tous les caractères utilisant le 8ème bit peuvent être mal affichés, si le fichier texte a été créé par un programme qui n'a pas été écrit en C.

Écrire un fichier texte

Écrire au début du fichier

Les formalités d'ouverture étant écrites, il ne reste plus qu'à écrire le code pour écrire dans le fichier, ligne par ligne.

On prévient l'utilisateur que s'il tape "fin", l'enregistrement prendra fin.

Puisque le programme devra écrire plusieurs lignes, dont on ignore le nombre, on utilise donc une boucle while(1). La condition est donc toujours vraie. Dans le corps de cette boucle, on place alors un if qui permettra de quitter cette boucle (break;)

La boucle commence par afficher une invitation suivie de la fonction fgets() qui place ce que l'utilisateur a tapé dans la variable ligne

La fonction strcmp() compare la ligne tapée avec le mot "fin". Elle retourne 0 s'il n'existe aucune différence. !0 = vrai.

Si l'utilisateur a tapé "fin", on quitte la boucle. Sinon, via la fonction strcat() on concatène deux string (on ajoute à la première string "\n"). Une fois, le retour de ligne ajouté, on "imprime" dans le fichier via la fonction fprintf(). Cette fonction ressemble à printf() qui imprime à l'écran, si ce n'est que le premier paramètre est ici un pointeur de fichier.

Enfin, après être sorti de la boucle, on n'oublie pas de vider la mémoire clavier, ni de fermer le fichier.

Cette fonction écrira au début ou à la fin du fichier. Pour faire la distinction, la fonction utilisera un paramètre. La valeur de ce paramètre (ici, mode) vaut la string "w" (= écrire au début) ou "a" (= écrire à la fin). Ce paramètre est du même "type" que la fonction qui la recevra : ouvrirFichier().

Puisque la fonction ouvrirFichier() ne peut recevoir qu'une string et que la fonction ecrire() lui transmet son paramètre, ce paramètre doit aussi être une string. Même si le contenu de cette string n'a qu'un seul caractère ("w" ou "a"), on ne peut pas la remplacer par une variable de type char (ayant comme contenu un caractère 'w' ou 'a')

void ecrire(char mode[]){

  if(!ouvrirFichier(mode)) return;

  printf("Taper 'fin' pour fermer le fichier\n");

  while(1){
    printf("Ligne A enregistrer : ");
    fgets(ligne, MAX, stdin);
    if(!strcmp(ligne,"fin\n"))break;
    fputs(ligne,pF);
  }

  fflush(stdin);// pour libérer la mémoire-clavier
  fclose(pF);// pour libérer la mémoire-fichier
}

Écrire à la fin du fichier

Par rapport à "écrire au début du fichier", seul le mode d'ouverture du fichier varie.

Les fichiers binaires

Lire ou écrire un fichier binaire "pur" ne présente pas d'intérêt (pour un débutant). Toutefois, il existe des fichiers binaires "texte". La différence entre un fichier texte (ordinaire) et un fichier texte (binaire) est que, dans ce type de fichier binaire, toutes les lignes ont la même longueur. Une ligne est alors appelée "enregistrement".

Ce type de fichier sert à stocker des données et travaille, en binôme, avec un ou plusieurs autres fichiers, appelés "index".

Un fichier index contient une liste de mots triés alphabétiquement et, sur la même ligne, se trouve un numéro d'enregistrement.

Supposons que vous souhaitez obtenir une adresse postale depuis un nom de personne. Le programme (écrit en C), va parcourir le petit fichier index, trouver le nom de la personne, puis le numéro d'enregistrement. Le programme ouvrira alors le fichier principal, multipliera ce numéro par la taille d'un enregistrement, placera le pointeur sur l'octet correspondant au produit de cette multiplication, lira l'enregistrement et l'affichera.

Cette technique permet de trouver un enregistrement parmi des millions, sans devoir lire tout le volumineux fichier principal. Cette technique est celle appliquée par les systèmes de base de données, telles que SQLite et MySQL (tous deux écrits en C).

Lire et écrire dans des fichiers

XOR

L'opérateur ^, appelé XOR, utilise deux opérandes; comme, pour l'addition, l'opérateur +, appelé PLUS. Toutefois, ici, les deux opérandes ne sont pas deux nombres, mais deux octets.

opérande : nom masculin (!). Élément sur lequel porte un opérateur mathématique ou informatique. Dans l'opération "4 + 3 = 7", "4" et "3" sont des opérandes, "+" est l'opérateur.

Comme le résultat de l'opérateur + est un autre nombre (sauf si un des opérandes vaut zéro), le résultat de l'opérateur ^ est un autre octet (sauf si le second opérande vaut zéro)

Pour créer un nouvel octet, cet opérateur XOR recopie tous les bits du premier octet, sauf si le bit correspondant du second octet vaut 1, auquel cas le bit du premier octet est inversé. Le second octet est souvent appelé masque (ou filtre).

Rappel. La position d'un bit se compte à partir de la droite.

Exemple 1

        1111 1111 // = 255 = octetAModifier
      ^ 0100 0000 // = 64  = octetMasque ( 7ème bit = 1, tous les autres = 0)
        ---------
        1011 1111 // = 191 = octetModifie (le 7ème bit a été inversé, par rapport au premier octet)

Exemple 2

        0000 0000 // = 0  = octetAModifier
      ^ 0100 0000 // = 64 = octetMasque
        ---------
        0100 0000 // = 64 = octetModifie (le 7ème bit a été inversé, par rapport au premier octet)

Exemple 3

        0100 0001 // = 65 = octetAModifier
      ^ 0100 0000 // = 64 = octetMasque
        ---------
        0000 0001 // = 1  = octetModifie (le 7ème bit a été inversé, par rapport au premier octet)

XOR devrait être interdit par la loi !

Vous savez qu'un octet est une série de 8 bits et qu'on peut y stocker un nombre entier allant de 0 à 255. Vous savez qu'une table ASCII est une table de concordance entre des nombres codés sur un octet et des caractères. Vous savez donc que si on modifie un seul bit, on obtient un autre nombre et donc un autre caractère.

    //------------------ coeur du programme --------------------

    char carac=0;
    printf("Tapez un caractEre : "); carac=getchar();
    printf("\nCaractEre modifiE : '%c'",carac^64);

    //--------------- fin du coeur du programme -----------------

Si le masque vaut 0100 0001 = 65, le résultat sera le premier octet dont les bits 1 et 7 ont été inversés.

Maintenant, si vous modifiez tous les octets d'un fichier, il sera crypté.

Et, vous savez que si on inverse deux fois un bit, on revient à la situation initiale. Donc, en utilisant deux fois l'opérateur XOR avec le même masque sur tous les octets d'un fichier, il sera décrypté. Ici, pour décrypter, il suffira de re-crypter, le fichier crypté. On pourra donc réutiliser la même fonction de cryptage.

Cette fonction va donc lire un fichier et le réécrire après avoir modifié tous les octets. Le mode d'ouverture du fichier pour ce type d'opération est "r+b" (= lire et modifier un fichier binaire)

Premièrement, on détermine la taille du fichier (= son nombre d'octets) afin de créer un tableau de char de même taille, (appelé ici contenu).

Ensuite, on met tous les octets du fichier dans ce tableau via l'instruction :
fread(&contenu,TAILLEFICHIER,1,pF);

Puis, on modifie tous les octets de ce tableau via la boucle :
for(int i=0;i<TAILLEFICHIER;i++) contenu[i]=contenu[i]^masque;

Enfin, on met tous les octets (modifiés) du tableau dans le fichier, via l'instruction :
fwrite(contenu,TAILLEFICHIER,1,pF);

Avec les fichiers binaires, on utilise des fonctions propres aux fichiers binaires

fseek() : déplace la "tête de lecture". Elle utilise trois paramètres. Le pointeur de fichier, la longueur de déplacement et la position actuelle. La longueur de déplacement et la position s'exprime en nombre entier (int). SEEK_END vaut fin de fichier.

fseek(pF,0,SEEK_END); place la tête de lecture à la fin du fichier

ftell() : donne en nombre entier (int) la position actuelle de la tête de lecture. Elle prend comme unique paramètre, le pointeur de fichier.

Demander la position de la tête de lecture après l'avoir mise à la fin du fichier permet de connaître le nombre d'octets contenu dans ce fichier. Cette taille du fichier ne variera pas. Elle est donc stockée dans une constante, appelée TAILLEFICHIER.

On affiche cette taille et un avertissement.

Si l'utilisateur persévère, un tableau de char est créé.

Comme la tête de lecture est toujours à la fin du fichier, on la replace au début pour pouvoir commencer la lecture.
fseek(pF,0,0);

Après avoir rempli le tableau, on demande à l'utilisateur sa clé de cryptage/décryptage. En fait, le masque qui sera utilisé avec l'opérateur XOR.

Comme après avoir rempli le tableau, la tête de lecture est à nouveau à la fin du fichier, on la replace au début pour pouvoir commencer écriture.

Enfin, on n'oublie pas de fermer le fichier. Puis, on affiche un message à l'utilisateur signalant la fin de l'opération (de cryptage/décryptage)

void crypter(){

  if(!ouvrirFichier("r+b")) return;

  fseek(pF,0,SEEK_END);
  unsigned int const TAILLEFICHIER = ftell(pF);
  printf(" = %d octets\n", TAILLEFICHIER);

  printf("Attention. Tous les octets vont Etre modifiEs.\n");
  char continuer='N';
  printf("Souhaitez vous continuez ? (y/N) : ");
  continuer=getchar();
  if(continuer!='y') return;

  char contenu[TAILLEFICHIER];
  fseek(pF,0,0);
  fread(&contenu,TAILLEFICHIER,1,pF);

  printf("ClE de cryptage/dEcryptage (1 A 255) = ");
  int masque; // si masque vaut 0 => pas de (dé)cryptage
  scanf("%d",&masque); fflush(stdin);

  for(int i=0;i<TAILLEFICHIER;i++) contenu[i]=contenu[i]^masque;

  fseek(pF,0,0);
  fwrite(contenu,TAILLEFICHIER,1,pF);

  fclose(pF);// pour libérer la mémoire/buffer
  printf("Fichier cryptE / dEcryptE");
}

Éditeur C

32 ou 64 bits

Pour distinguer les programmes compilés pour des processeurs 32-bits ou 64-bits, il faut lancer ce programme. Puis, sous Windows 64-bits, dans le gestionnaire des tâches (qui s'ouvre via CTRL+ALT+DEL), sous l'onglet "Processus", s'affiche une liste d' "applications". Celles qui, après leur nom, n'affichent pas "32-bits" sont des applications "64-bits".

monEditeurC.exe est une application-console 64 bits.

Cryptographie

Faille de sécurité. Certes, votre fichier sera bien crypté. Mais, si vous cryptez un fichier texte, la sécurité est compromise. En effet, sous windows, la fin de ligne est codée sur deux octets (codé en hexadécimal : 0D 0A )

Sous linux, la fin de ligne est codée sur un octet (codé en hexadécimal  : 0A )

Il suffit donc de comparer le code du dernier octet (crypté) avec celui du dernier octet (non crypté) pour déduire le masque utilisé avec l'opérateur XOR.

Certes, les non-initiés ne le savent pas. Mais, tous les programmeurs le savent.

Il faudrait, a minima, supprimer cette fin de ligne à la fin du fichier lors du cryptage d'un fichier texte et le restaurer lors du décryptage.

L'autre faille est que le cryptage utilise comme masque qu'un octet. Si le masque correspond au nombre 0, l'opérateur XOR sera inefficace. Il n'existe donc que 255 combinaisons ...

Si vous cryptez sur 2 octets (avec deux masques), le nombre de combinaisons est 65 025; sur 3 octets, 16 581 375 combinaisons; sur 4 octets => 4 228 250 625 combinaisons; ... Mais, vous devrez alors gérer le fait que la taille d'un fichier n'est pas nécessairement divisible par 2, 3 ou 4. Le fichier crypté sera alors légèrement plus grand que le fichier initial.

Cryptographie

La police peut exiger vos clés de chiffrement si elle vous soupçonne d'activités illégales. Le refus de collaboration est punissable à hauteur de cinq ans de prison et 75.000 euros d'amende (en France)