Lire un fichier binaire et extraire ses informations

Le problème exposé dans ce sujet a été résolu.

Bonjour,

J’essaye d’extraire les 3 premiers nombres (encodés en Int32) d’un fichier binaire.
Ces nombres correspondent à des données utiles (N° de version, type de base de donnée, etc).

Sur l’image ci-dessous, on peut par exemple lire que les 3 premiers nombres sont 4, 1 et 9 :

Capture.PNG
Capture.PNG

Le problème, c’est que je ne comprends pas du tout pourquoi mon code échoue.
Il renvoie les nombres 0, 0 et 0. Pourtant, je lis bien des entiers 32 bits il me semble.

Une idée d’où pourrait venir le problème ?

#include <iostream>
#include <fstream>

using std::ios;
using std::cout;
using std::endl;
using std::ifstream;


int main()
{
    ifstream f_pin("sdb.fasta.pin", ios::in | ios::binary);
    
    if (!f_pin.is_open())
    {
        cout << "Impossible d'ouvrir le fichier binaire !" << endl;
        return 1;
    }

    int32_t version;
    int32_t db_type;
    int32_t title_length;

    f_pin >> version;
    f_pin >> db_type;
    f_pin >> title_length;
    
    cout    << "Version de la base de donnée :    " << version      << endl
            << "Type de BDD (0 ou 1) :            " << db_type      << endl
            << "Longeur du titre :                " << title_length << endl
            ;
    
    /**
     *  Ouput attendu :
     * 
     *    Version de la base de donnée :    4
     *    Type de BDD (0 ou 1) :            1
     *    Longeur du titre :                (à déterminer à l'exécution)
     *
     */
    
    f_pin.close();
    return 0;
}

Le fichier est en big endian (le format de stockage probablement le plus répandu) alors que tu es sur une machine little-endian (l’architecture dominante aujourd’hui) ?

EDIT: Pardon, j’avais raté un gros détail. Les opérateurs formatés << et >>, c’est fait pour manipuler du texte. Pas du binaire. Pour du binaire, c’est *stream::read() et *stream::write() qu’il faut employer, et ensuite possiblement utiliser les fonctions de conversion netbyte-order to native-host

Sans passer par des libs qui le font pour toi, non.

P.ex., un truc que j’ai pondu vite fait il y a peu: https://openclassrooms.com/forum/sujet/convertir-un-unsigned-32-bit-en-little-endian#message-93649342

La différence, est que tu veux le contraire: read + ntoh (au lieu de hton).

// host to net
std::uint8_t  hton(std::uint8_t v)  { return v; }
std::uint16_t hton(std::uint16_t v) { return htons(v); }
std::uint32_t hton(std::uint32_t v) { return htonl(v); }

// net to host
std::uint8_t  ntoh(std::uint8_t v)  { return v; }
std::uint16_t ntoh(std::uint16_t v) { return ntohs(v); }
std::uint32_t ntoh(std::uint32_t v) { return ntohl(v); }

template <typename T>
T read(std::istream & is) {
    static_assert(std::is_integral<T>::value, "faut des types entiers");
    T v;
    f.read(reinterpret_cast<char*>(&v), sizeof(v));
    return ntoh(v);
}

Mais s’il y a beaucoup de choses à lire, il sera plus intelligent de lire dans un vecteur ou un array de la bonne taille, et de décoder directement dedans. Car multiplier les read() ne sera pas très efficace AMA.

Je suis toujours coincé, j’ai envie de me péter la tête.

J’ai essayé en utilisant la fonction reinterpret_cast mais elle ne fonctionne pas, j’ai décidé de la laisser tomber. Je vais manuellement effectuer l’opération pour extraire, car toutes les autres solutions envisagées ont échouées.

Voici donc ce que j’essaye de faire : lire les 4 premiers octets de mon fichier binaire et interpréter la valeur en tant que Int32.

Pour cela, je lis les 4 premiers octets que je place dans un char buffer[4];. Ensuite, j’effectue la conversion, a priori c’est donc un simple changement de base.

Pourtant, le code renvoie des valeurs complètement loufoques.

#include <iostream>
#include <fstream>

using std::ios;
using std::cout;
using std::endl;
using std::ifstream;


uint32_t my_convert(char* buffer)
{
    return buffer[3] + 255*buffer[2] + 255*255*buffer[1] + 255*255*255*buffer[0];
}


int main()
{
    ifstream f_pin("sdb.fasta.pin", ios::in | ios::binary);
    
    if (!f_pin.is_open())
    {
        cout << "Impossible d'ouvrir le fichier binaire !" << endl;
        return 1;
    }
    
    
    char* buffer = new char[4];
    f_pin.read(buffer, 4);
    
    uint32_t version = my_convert(buffer);
    
    
    cout    << "Version de la base de donnée :    " << version << endl;
            ;
        
    
    delete[] buffer;
    f_pin.close();
    return 0;
}

Pourquoi une allocation? Pour seulement 4 octets.

Et pourquoi ne pas utiliser les fonctions dédiées de conversion bigendian vers host? Elles sont éprouvées et non bugguées. Ta formule est fausse p.ex. Avec un man ntohl dans ta console tu sauras quel include charger sur ton système. Sinon, il y a boost.Endian en portable.

Et définis "ne marche pas"

Avec mon exemple, ça se faisait avec un

std::ifstream f_pin("sdb.fasta.pin", std::ios::binary); // "in" est implicite sur un Input-stream
    
if (!f_pin) // test idiomatique sur les fichiers
{
    std::cerr << "Impossible d'ouvrir le fichier binaire !\n" ;
    return EXIT_FAILURE;
    // ou mieux: exception pour tout centraliser
}

auto const version = read<std::uint32_t>(f_pin);

// pas besoin de close, bienvenu en C++.

Au départ, j’ai hésité à copier-coller ton code mais c’est risqué d’un point de vue plagiat et également pour la défense orale du projet. J’ai réussi à corriger mon code en prenant en compte la plupart de tes remarques, et je te remercie pour cela. :)

Finalement, j’ai opté pour ce code ci-dessous que j’intégrerai au projet.
J’ai préféré écrire un code que je comprends ligne par ligne.

#define  BUFFER_SIZE 4
#include <iostream>
#include <fstream>

using std::cout;
using std::endl;
using std::ifstream;


/**
 *   Cette fonction convertit une chaîne de caractères représentant
 *   4 octets vers la valeur Int32 (big endian) correspondante.
 * 
 *   Les éventuels caractères excédentaires sont ignorés.
 **/
int32_t my_convert(const char* buffer)
{
    return    buffer[3]
            + buffer[2]*256
            + buffer[1]*256*256
            + buffer[0]*256*256*256;
}




int main()
{
    ifstream f_pin("sdb.fasta.pin", std::ios::binary);
    
    if (!f_pin.is_open()) {
        cout << "Impossible d'ouvrir le fichier binaire !" << endl;
        return EXIT_FAILURE;
    }
    
    
    char buffer[BUFFER_SIZE];
    
    f_pin.read(buffer, BUFFER_SIZE);
    int32_t version = my_convert(buffer);
    
    f_pin.read(buffer, BUFFER_SIZE);
    int32_t typeDB = my_convert(buffer);
    
    f_pin.read(buffer, BUFFER_SIZE);
    const int32_t title_length = my_convert(buffer);


    char title[title_length + 1]; // +1 pour le caractère de fin de chaîne
    
    f_pin.read(title, title_length);
    title[title_length] = '\0';


    cout    << "Version de la base de donnée :    " << version  << endl
            << "Type de DB (0 ou 1) :             " << typeDB   << endl
            << "Titre de la base de donnée :      " << title    << endl
            ;


    /**
     *    Note that any open file is automatically closed when the ifstream object is destroyed.
     *    http://www.cplusplus.com/reference/fstream/ifstream/close/
     *
     *    => L'instruction f_pin.close(); est implicite ici.
     */

    return EXIT_SUCCESS;
}

A moins d’être sous Windows (et encore c’est une question d’include), il n’y a aucune raison à ne pas utiliser les fonctions ntoh*

PS: en ce qui me concerne, pour ces bouts de code, je copie-colle ce j’ai mis dans mon blog:

Last thing, as French law doesn’t let me put all my code snippets under Public Domain, let’s say that they are available under BSD 3 clauses license, and under MIT license, and under Boost Software License, and under GPL v3 license, and even under CC 0. Choose the one that fits you best.

Bonjour Green, Je me pose une question au sujet de ton code:

int32_t my_convert(const char* buffer)
{
    return    buffer[3]
            + buffer[2]*256
            + buffer[1]*256*256
            + buffer[0]*256*256*256;
}

Ne vaudrait-il pas mieux utiliser un tableau de unsigned char ou de uint_8, plutôt que de char ? En effet, tu vas avoir une conversion de type de tes 4 "char" en "int", et a ce moment là, si l’un de tes "char" est négatif, il va y avoir extension de signe, jusqu’au bit 32 … Je prend un exemple:

  • si buffer = {0,0,0,-1}, le résultat de ta fonction vaudra -1 (c’est à dire 0xFFFFFFFF)
  • si buffer = {0,0,1,-1}, le résultat de ta fonction vaudra 255 (c’est à dire 0x000000FF)
  • … Je pense que ce n’est pas ce que tu veux. Par contre avec ce code:
#include <iostream>

int32_t my_convert(const unsigned char* buffer)
{
    return    buffer[3]
            + buffer[2]*256
            + buffer[1]*256*256
            + buffer[0]*256*256*256;
}

int main()
{
    const unsigned char buffer [4] = {0,0,1,255};
    const int32_t value1 = my_convert(buffer);

    std::cout    << "Valeur 1  =  " << value1  << "(0x"<< std::hex << value1  << ")" << std::dec << std::endl;
    return 0;
}

tu vas trouver (cas identique à ci dessus)

  • si buffer = {0,0,0,255}, le résultat de ta fonction vaudra 255 (c’est à dire 0xFF)
  • si buffer = {0,0,1,255}, le résultat de ta fonction vaudra 511 (c’est à dire 0x1FF)

et là, c’est mieux.

Cordialement.

+0 -0

Bonsoir,

Merci pour vos remarques, en effet, vous avez raison.
J’ai du mal à ajouter le mot-clef unsigned au dernier code, la compilation échoue lignes 39, 42 et 45.

Néanmoins, j’ai pu corriger le code « à la main » comme ceci :

int32_t my_convert(const char* buffer)
{
    int32_t add = 0;
    
    if (buffer[3] < 0)
        add += 256;
    
    if (buffer[2] < 0)
        add += 256*256;
    
    if (buffer[1] < 0)
        add += 256*256*256;
    
    if (buffer[0] < 0)
        add += 256*256*256*256;
    
    return    buffer[3]
            + buffer[2]*256
            + buffer[1]*256*256
            + buffer[0]*256*256*256
            + add;
}

Ce n’est pas parfait, mais ça fera le taff normalement.

Sérieusement, c’est quoi le problème avec l’utilisation d’une solution standard (POSIX) et portable?

std::uint32_t v;
f.read(reinterpret_cast<char*>(&v), sizeof(v));
v = ntohl(v);

(Bon après, l’utilisation de surcharges renforce le typage en factorisant et évitant les erreurs de mismatch…)

PS: les char* sont le format officiel pour les trames binaires, mais… c’est la version unsigned qu’il faudrait manipuler car elle assure des comportements bien définis quand on recompose.

EDIT: Et ne parlons même pas de l’efficacité des compilateurs à traiter efficacement la fonction qu’ils connaissent. Ou est-ce dans l’implémentation? Bref, Les fonctions ntoh* sont sans commune mesure supérieures aux implémentations manuelles: https://godbolt.org/z/wzcspZ Parenthèse, sur une machine bigendian nous n’aurions aucune aucune instruction de transformation. C’est tout l’intérêt des fonctions net to host ou host to net — il en existe aussi pour forcer le dialogue avec des trames sérialisée en little endian.

Au passage on voit un joli integer overflow sur 256*256*256*256

Alors? Une solution éprouvée? Robuste? Ultra-efficace? Connue? Standard? Non, vraiment pas?

@lmghs Je comprends tes arguments, dans l’idéal cette solution standard est meilleure, oui.

Malheureusement, la date de remise approche et je n’arrive pas à comprendre ce que font ces fonctions et comment les utiliser. C’est quoi le net, le host, quelles fonctions choisir où et quand ? Elles n’ont même pas été vues au cours. Par contre, ça donne des idées d’amélioration pour le futur. En attendant, je me contente d’un code vite fait, pas parfait, mais il fonctionne dans un premier temps.

L’hôte, c’est la machine courante. Quel que soit son endianisme.

Le net, c’est en référence au NETwork byte order. Qui est l’endianisme typique des machins sérialisés sur le réseau ou sur disque. Son avantage est qu’il est humainement lisible: le poids fort est mis devant: quand on lit "a b c d", numériquement cela vaut ((a*256+b)*256+c)*256+d. Exactement comme pour "1234" qui s’évalue en ((1*10+2)*10+3)*10+4.

Les PC sont en little endian, ce qui veut dire que le poids fort est à la fin au lieu d’être au début. C’est une optim qui facilite la propagation des retenues sur les opérations arithmétiques.

Et les fonctions ntoh/hton ont l’avantage de traduire entre une cible de sérialisation bigendian (aka network byte order) comme c’est ton cas, et la machine courante (host) sans avoir à se poser de questions.

Chercher à savoir si l’hôte est en big ou little, ou décoder à la main, c’est se compliquer la vie, risquer d’introduire des bugs (cas de tes deux précédentes versions).

Bonjour,

Je me permets une petite correction au sujet de des "petits indiens" et des "grand indiens"

Les PC sont en little endian, ce qui veut dire que le poids fort est à la fin au lieu d’être au début. C’est une optim qui facilite la propagation des retenues sur les opérations arithmétiques.

lmghs

Je me permets de faire référence à cette discussion, …. et à mettre au passé l’affirmation au sujet de l’optimisation: Ca a été, au début de l’informatique, il y a bien longtemps, une optimisation. Mais maintenant c’est historique et pour garder la compatibilité. Ça n’a plus s’impacte.

Cordialement.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte