web-dev-qa-db-fra.com

Analyser un fichier binaire. Qu'est-ce qu'une manière moderne?

J'ai un fichier binaire avec une mise en page que je connais. Par exemple, laissez le format être comme ceci:

  • 2 octets (court non signé) - longueur d'une chaîne
  • 5 octets (5 x caractères) - la chaîne - un nom d'identification
  • 4 octets (entier non signé) - une foulée
  • 24 octets (6 x float - 2 foulées de 3 float chacun) - données float

Le fichier devrait ressembler à (j'ai ajouté des espaces pour plus de lisibilité):

5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5

Ici 5 - est de 2 octets: 0x05 0x00. "bonjour" - 5 octets et ainsi de suite.

Maintenant, je veux lire ce fichier. Actuellement, je le fais:

  • charger le fichier dans ifstream
  • lire ce flux dans char buffer[2]
  • transformez-le en short non signé: unsigned short len{ *((unsigned short*)buffer) };. Maintenant, j'ai une longueur de chaîne.
  • lire un flux vers vector<char> et créer un std::string à partir de ce vecteur. Maintenant, j'ai un identifiant de chaîne.
  • de la même manière, lisez les 4 octets suivants et convertissez-les en entier non signé. Maintenant, j'ai une foulée.
  • alors qu'il n'est pas en fin de fichier, les flotteurs lus de la même manière - créez un char bufferFloat[4] et transtypez *((float*)bufferFloat) pour chaque flottant.

Cela fonctionne, mais pour moi ça a l'air moche. Puis-je lire directement dans unsigned short Ou float ou string etc. sans que char [x] Ne crée? Si non, quelle est la façon de lancer correctement (j'ai lu le style que j'utilise - est un ancien style)?

P.S .: pendant que j'écrivais une question, l'explication la plus claire soulevée dans ma tête - comment convertir un nombre arbitraire d'octets à partir d'une position arbitraire dans char [x]?

Mise à jour: j'ai oublié de mentionner explicitement que la longueur des données de chaîne et de flottant n'est pas connue au moment de la compilation et est variable.

46
nikitablack

La manière C, qui fonctionnerait bien en C++, serait de déclarer une structure:

#pragma pack(1)

struct contents {
   // data members;
};

Notez que

  • Vous devez utiliser un pragma pour faire en sorte que le compilateur aligne les données telles qu'elles paraissent dans la structure;
  • Cette technique ne fonctionne qu'avec types POD

Et puis convertissez le tampon de lecture directement dans le type de structure:

std::vector<char> buf(sizeof(contents));
file.read(buf.data(), buf.size());
contents *stuff = reinterpret_cast<contents *>(buf.data());

Maintenant, si la taille de vos données est variable, vous pouvez les séparer en plusieurs morceaux. Pour lire un seul objet binaire à partir du tampon, une fonction de lecture est pratique:

template<typename T>
const char *read_object(const char *buffer, T& target) {
    target = *reinterpret_cast<const T*>(buffer);
    return buffer + sizeof(T);
}

Le principal avantage est qu'un tel lecteur peut être spécialisé pour des objets c ++ plus avancés:

template<typename CT>
const char *read_object(const char *buffer, std::vector<CT>& target) {
    size_t size = target.size();
    CT const *buf_start = reinterpret_cast<const CT*>(buffer);
    std::copy(buf_start, buf_start + size, target.begin());
    return buffer + size * sizeof(CT);
}

Et maintenant dans votre analyseur principal:

int n_floats;
iter = read_object(iter, n_floats);
std::vector<float> my_floats(n_floats);
iter = read_object(iter, my_floats);

Remarque: Comme l'a observé Tony D, même si vous pouvez obtenir l'alignement correct via #pragma directives et remplissage manuel (si nécessaire), vous pouvez toujours rencontrer une incompatibilité avec l'alignement de votre processeur, sous la forme de problèmes de performances (dans le meilleur des cas) ou de signaux de piège (dans le pire des cas). Cette méthode n'est probablement intéressante que si vous contrôlez le format du fichier.

9
slaphappy

Si ce n'est pas à des fins d'apprentissage, et si vous êtes libre de choisir le format binaire, vous feriez mieux d'envisager d'utiliser quelque chose comme protobuf qui gérera la sérialisation pour vous et permettra d'interagir avec d'autres plateformes et langages.

Si vous ne pouvez pas utiliser une API tierce, vous pouvez consulter QDataStream pour vous inspirer

13
fjardon

J'ai en fait implémenté un analyseur de format binaire rapide et sale pour lire .Zip fichiers (suivant la description du format de Wikipédia) le mois dernier, et étant moderne, j'ai décidé d'utiliser des modèles C++.

Sur certaines plates-formes spécifiques, un struct compressé pourrait fonctionner, mais il y a des choses qu'il ne gère pas bien ... comme les champs de longueur variable. Avec les modèles, cependant, il n'y a pas un tel problème: vous pouvez obtenir des structures arbitrairement complexes (et renvoyer des types).

UNE .Zip l'archive est relativement simple, heureusement, j'ai donc implémenté quelque chose de simple. Du haut de ma tête:

using Buffer = std::pair<unsigned char const*, size_t>;

template <typename OffsetReader>
class UInt16LEReader: private OffsetReader {
public:
    UInt16LEReader() {}
    explicit UInt16LEReader(OffsetReader const or): OffsetReader(or) {}

    uint16_t read(Buffer const& buffer) const {
        OffsetReader const& or = *this;

        size_t const offset = or.read(buffer);
        assert(offset <= buffer.second && "Incorrect offset");
        assert(offset + 2 <= buffer.second && "Too short buffer");

        unsigned char const* begin = buffer.first + offset;

        // http://commandcenter.blogspot.fr/2012/04/byte-order-fallacy.html
        return (uint16_t(begin[0]) << 0)
             + (uint16_t(begin[1]) << 8);
    }
}; // class UInt16LEReader

// Declined for UInt[8|16|32][LE|BE]...

Bien sûr, la base OffsetReader a en fait un résultat constant:

template <size_t O>
class FixedOffsetReader {
public:
    size_t read(Buffer const&) const { return O; }
}; // class FixedOffsetReader

et puisque nous parlons de modèles, vous pouvez changer les types à loisir (vous pouvez implémenter un lecteur proxy qui délègue toutes les lectures à un shared_ptr qui les mémorise).

Ce qui est intéressant, cependant, c'est le résultat final:

// http://en.wikipedia.org/wiki/Zip_%28file_format%29#File_headers
class LocalFileHeader {
public:
    template <size_t O>
    using UInt32 = UInt32LEReader<FixedOffsetReader<O>>;
    template <size_t O>
    using UInt16 = UInt16LEReader<FixedOffsetReader<O>>;

    UInt32< 0> signature;
    UInt16< 4> versionNeededToExtract;
    UInt16< 6> generalPurposeBitFlag;
    UInt16< 8> compressionMethod;
    UInt16<10> fileLastModificationTime;
    UInt16<12> fileLastModificationDate;
    UInt32<14> crc32;
    UInt32<18> compressedSize;
    UInt32<22> uncompressedSize;

    using FileNameLength = UInt16<26>;
    using ExtraFieldLength = UInt16<28>;

    using FileName = StringReader<FixedOffsetReader<30>, FileNameLength>;

    using ExtraField = StringReader<
        CombinedAdd<FixedOffsetReader<30>, FileNameLength>,
        ExtraFieldLength
    >;

    FileName filename;
    ExtraField extraField;
}; // class LocalFileHeader

C'est plutôt simpliste, évidemment, mais incroyablement flexible en même temps.

Un axe d'amélioration évident serait d'améliorer chaînage puisqu'il y a ici un risque de chevauchements accidentels. Mon code de lecture d'archive a fonctionné la première fois que je l'ai essayé, ce qui était une preuve suffisante pour moi que ce code était suffisant pour la tâche à accomplir.

7
Matthieu M.

J'ai dû résoudre ce problème une fois. Les fichiers de données ont été compressés en sortie FORTRAN. Les alignements étaient tous erronés. J'ai réussi avec des astuces de préprocesseur qui ont fait automatiquement ce que vous faites manuellement: décompresser les données brutes d'un tampon d'octets dans une structure. L'idée est de décrire les données dans un fichier include:

BEGIN_STRUCT(foo)
    UNSIGNED_SHORT(length)
    STRING_FIELD(length, label)
    UNSIGNED_INT(stride)
    FLOAT_ARRAY(3 * stride)
END_STRUCT(foo)

Vous pouvez maintenant définir ces macros pour générer le code dont vous avez besoin, par exemple la déclaration struct, inclure ce qui précède, annuler la définition et redéfinir les macros pour générer des fonctions de décompression, suivies d'une autre inclusion, etc.

NB J'ai vu pour la première fois cette technique utilisée dans gcc pour la génération de code lié à l'arbre de syntaxe abstraite.

Si CPP n'est pas assez puissant (ou qu'un tel abus de préprocesseur n'est pas pour vous), remplacez un petit programme Lex/yacc (ou choisissez votre outil préféré).

C'est incroyable pour moi combien de fois il est avantageux de penser en termes de génération de code plutôt que de l'écrire à la main, au moins dans un code de base de bas niveau comme celui-ci.

3
Gene

Vous devriez mieux déclarer une structure (avec un remplissage à 1 octet - comment - dépend du compilateur). Écrivez en utilisant cette structure et lisez en utilisant la même structure. Mettez seulement POD dans la structure, et donc pas de std::string etc. Utilisez cette structure uniquement pour les E/S de fichiers ou toute autre communication inter-processus - utilisez la normale struct ou class pour la conserver pour une utilisation ultérieure dans le programme C++.

2
Ajay

Étant donné que toutes vos données sont variables, vous pouvez lire les deux blocs séparément et toujours utiliser la conversion:

struct id_contents
{
    uint16_t len;
    char id[];
} __attribute__((packed)); // assuming gcc, ymmv

struct data_contents
{
    uint32_t stride;
    float data[];
} __attribute__((packed)); // assuming gcc, ymmv

class my_row
{
    const id_contents* id_;
    const data_contents* data_;
    size_t len;

public:
    my_row(const char* buffer) {
        id_= reinterpret_cast<const id_contents*>(buffer);
        size_ = sizeof(*id_) + id_->len;
        data_ = reinterpret_cast<const data_contents*>(buffer + size_);
        size_ += sizeof(*data_) + 
            data_->stride * sizeof(float); // or however many, 3*float?

    }

    size_t size() const { return size_; }
};

De cette façon, vous pouvez utiliser la réponse de M. kbok pour analyser correctement:

const char* buffer = getPointerToDataSomehow();

my_row data1(buffer);
buffer += data1.size();

my_row data2(buffer);
buffer += data2.size();

// etc.
2
Barry

Personnellement, je le fais de cette façon:

// some code which loads the file in memory
#pragma pack(Push, 1)
struct someFile { int a, b, c; char d[0xEF]; };
#pragma pack(pop)

someFile* f = (someFile*) (file_in_memory);
int filePropertyA = f->a;

Moyen très efficace pour les structures de taille fixe au début du fichier.

2
rev

Utilisez une bibliothèque de sérialisation. Voici quelques-uns:

1
Átila Neves

J'utilise l'outil ragel pour générer du code source procédural C pur (pas de tables) pour les microcontrôleurs avec 1-2K de RAM. Il n'utilisait aucun fichier io, mise en mémoire tampon, et produit à la fois du code facile à déboguer et un fichier .dot/.pdf avec le diagramme de la machine d'état.

ragel peut également sortir du code go, Java, .. pour l'analyse, mais je n'ai pas utilisé ces fonctionnalités.

La caractéristique clé de ragel est la possibilité d'analyser toutes les données de construction d'octets, mais vous ne pouvez pas creuser dans les champs de bits. Un autre problème est le ragel capable d'analyser les structures régulières mais n'a pas d'analyse de grammaire de récursivité et de syntaxe.

0
Dmitry Ponyatov