Greboca  

Suport technique et veille technologique

Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.

C’est alors la barrière de la prise en main qui fait peur, et pourtant...

Les logiciels libres

L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.

Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.

Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.

Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.

LinuxFr.org : les journaux  -  PullRequest d'une application en Rust

 -  16 mars - 

Sommaire

Si vous avez la flemme de lire, n'hésitez pas à aller directement à la fin. Je viens d'écrire un premier vrai programme en Rust et j'aimerais avoir des retours dessus. C'est l'objectif de ce journal.

Le commencement

J'utilise BackupPC pour sauvegarder mes données. BackupPC est un logiciel de sauvegarde qui se connecte à différents ordinateurs en SSH et utilise rsync pour sauvegarder les données. Il fonctionne parfaitement avec des ordinateurs Linux et un peu moins bien sur des ordinateurs Windows où il faut installer un rsyncd/Cygwin (les données ne sont pas protégées par SSH).

Par ailleurs, je développe mon propre logiciel de sauvegarde. C'est un challenge personnel que je me suis donné pour répondre à mes propres besoins (et aussi pour le plaisir).

Mon premier prototype est écrit en TypeScript et utilise rsync couplé avec Btrfs pour faire des sauvegardes incrémentales. Malheureusement, quelques problèmes liés à l'utilisation de Btrfs m'ont fait abandonner ce prototype (problème avec la création d'un grand nombre de snapshots et un système de fichier un peu trop plein). J'en parle dans un article sur mon blog.

Je me suis donc tourné vers l'écriture de mon propre pool de stockage. J'ai donc fait un second prototype, toujours en TypeScript, pour tester mon idée. Je suis content du résultat, mais les limites du moteur JavaScript ne font de ce prototype qu'un prototype. Là aussi j'en parle dans un autre article de mon blog.

Je vais donc réécrire la partie la plus importante de ce programme en Rust. Pourquoi Rust ? Dans mon enfance, j'ai adoré faire du C/C++. Je me souviens d'avoir programmé un petit IDE en C++ avec Qt. Lors du développement en C++, il m'arrivait parfois de me retrouver avec des fuites de mémoire, des segmentation faults, ainsi que des problèmes de concurrence d'accès aux données.

C++ m'a appris énormément sur le fonctionnement d'une machine, la gestion de la mémoire, la gestion du multithreading, …. Tout le monde devrait commencer par ce langage :D.

Rust est un langage qui a été conçu pour éviter ces problèmes. Après lecture de la documentation, j'ai adoré le concept. Du coup, j'ai décidé que ce serait une très bonne idée d'apprendre à l'utiliser. (Surtout qu'on en entend beaucoup parler en ce moment).

Vous trouvez que je digresse beaucoup ? C'est possible.

Retournons à notre programme.

Je me suis dit qu'avant d'écrire la gestion de mon pool de stockage en Rust, je souhaitais faire un script qui me permette de migrer le contenu de mon pool de stockage de BackupPC vers mon nouveau pool de stockage. Et de le faire en Rust. Pour cela, j'ai besoin de comprendre comment fonctionne le pool de stockage de BackupPC. De plus, faire du reverse engineering de BackupPC sera amusant (il y a le code source donc ce n'est pas trop compliqué).

Description du pool de stockage de BackupPC

La documentation de BackupPC décrit déjà pas mal de choses:

Ensuite, pour avoir les détails, il faut aller lire le code source en C qui sert de liaison avec le code en Perl. En effet, BackupPC est écrit en Perl et utilise un rsync modifié pour stocker les fichiers dans le pool. La bibliothèque sert donc au rsync modifié et au code Perl pour lire et écrire dans le pool.

Le format des fichiers compressés

Voici la description du fichier compressé d'après la documentation (traduction libre):

Le format de fichier compressé est généré par Compress::Zlib::deflate avec une modification mineure, mais importante.
Comme Compress::Zlib::inflate gonfle entièrement son argument en mémoire, il pourrait prendre de grandes quantités de mémoire s'il gonflait un fichier très compressé. Par exemple, un fichier de 200 Mo de 0x0 bytes se compresse à environ 200 Ko. Si Compress::Zlib::inflate était appelé avec ce seul tampon de 200 Ko, il aurait besoin d'allouer 200 Mo de mémoire pour retourner le résultat.

BackupPC surveille comment un fichier se compresse efficacement. Si un gros fichier a une compression très élevée (ce qui signifie qu'il utilisera trop de mémoire lorsqu'il sera gonflé), BackupPC appelle la méthode flush(), qui termine proprement la compression en cours. BackupPC commence alors une autre définition et ajoute simplement le fichier de sortie. Ainsi, le format de fichier compressé de BackupPC est une ou plusieurs définitions/flushes concaténées. Les ratios spécifiques que BackupPC utilise sont que si un morceau de 6 Mo se compresse à moins de 64 Ko, alors un flush sera effectué.

Donc, pour notre cas, il faut qu'on puisse lire un fichier compressé comme une suite de fichiers compressés concaténés les uns à la suite des autres.

Le format des fichiers d'attributs

Dans la documentation on y décrit que dans la version 4, on retrouve un fichier attrib_33fe8f9ae2f5cedbea63b9d3ea767ac0 dans les différents dossiers du PC. Le nom du
fichier contient le hash du fichier d'attributs que l'on peut retrouver dans le pool de stockage.

Il faut donc, pour accéder à un fichier du pool de stockage, aller dans le dossier __TOPDIR__/pc et lire le nom du fichier d'attribut pour retrouver le hash du fichier compressé correspondant au nom du dossier que l'on veut lire. Enfin, il faut lire le fichier compressé pour obtenir les données."

Le contenu du fichier d'attribut n'est pas décrit. Mais on peut le retrouver dans le fichier bpc_attribs.c.

Le fichier d'attribut est encodé en binaire. C'est une suite de varint (entier encodé sur un nombre variable d'octet). On peut décrire le fichier comme suite :

0x17565353 # Numéro magique

# Pour chaque fichier
varint longueur du nom du fichier
string nom du fichier
varint nombre d'entrées de type xattr
varint type de fichier
varint date du fichier
varint mode UNIX du fichier
varint uid du fichier
varint gid du fichier
varint taille du fichier
varint inode du fichier sauvegardé
varint niveau de compression du fichier
varint nombre de liens physiques sur le fichier
varint longueur du hash du fichier
buffer hash du fichier (MD5 utilisé pour le retrouver dans le pool)

# Pour chaque entrée de type xattr
varint longueur du nom de l'entrée
buffer nom de l'entrée
varint longueur de la valeur de l'entrée
buffer valeur de l'entrée

Il ne me reste donc plus qu'à reproduire tout cela en Rust.

Le développement en Rust

Pour développer le programme, j'ai posé quelques limites. Je me concentre uniquement sur la lecture des fichiers du pool qui sont en version 4. Ma version de BackupPC est en version 4, et j'ai migré tout le pool de stockage de la version 3. Je n'ai donc pas de données pour tester mon programme.

Les fichiers compressés

J'ai donc commencé par faire un programme qui, à partir d'un hash, est capable de décompresser un fichier de ce pool. Pour décompresser un fichier BackupPC compressé avec zlib, j'ai souhaité utiliser la bibliothèque flate2, qui est l'alternative à la bibliothèque standard zlib de Rust.

La bibliothèque flate2 permet de décompresser un fichier en utilisant la notion de buffer Rust. La version simple pour décompresser un fichier du pool est donc:

use flate2::bufread::ZlibDecoder;
use std::fs::File;

fn main() {
    let f = File::open("33fe8f9ae2f5cedbea63b9d3ea767ac0").unwrap();
    let b = BufReader::new(f);
    let mut z = ZlibEncoder::new(b);
    let mut buffer = Vec::new();
    z.read_to_end(&mut buffer).unwrap();
    println!("{:?}", buffer);
}

Malheureusement, ce code ne fonctionne pas. En effet, le fichier compressé est un ensemble de fichiers compressés. Pour un fichier relativement long (plusieurs Mo), il est possible que le fichier compressé soit compressé en plusieurs morceaux. De plus, il faut éviter de lire l'ensemble du fichier compressé en mémoire pour éviter de consommer toute la mémoire.

Lire le fichier bpc_fileZIO.c permet de comprendre comment sont encodés les fichiers compressés.

Lors de la lecture d'un fichier compressé, si la lecture de la compression s'arrête alors il faut reprendre la suite. On peut voir aussi le bout de code suivant:

// https://github.com/backuppc/backuppc-xs/blob/master/bpc_fileZIO.c#L219C15-L237C18
if ( fd->strm.next_in[0] == 0xd6 || fd->strm.next_in[0] == 0xd7 ) {
    /*
     * Flag 0xd6 or 0xd7 means this is a compressed file with
     * appended md4 block checksums for rsync.  Change
     * the first byte back to 0x78 and proceed.
     */
    fd->strm.next_in[0] = 0x78;
} else if ( fd->strm.next_in[0] == 0xb3 ) {
    /*
     * Flag 0xb3 means this is the start of the rsync
     * block checksums, so consider this as EOF for
     * the compressed file.  Also seek the file so
     * it is positioned at the 0xb3.
     */
    fd->eof = 1;
    /* TODO: check return status */
    lseek(fd->fd, -fd->strm.avail_in, SEEK_CUR);
    fd->strm.avail_in = 0;
}

Soit, si en début de flux, on trouve 0xd6 ou 0xd7, il faut changer le premier octet en 0x78 et continuer la lecture. En fin de flux, si on trouve 0xb3, on considère que c'est la fin du fichier compressé.

La bibliothèque flate2 ne permet pas de faire cela simplement. Il a donc fallu que je lise le code de flate2 pour comprendre comment je pouvais agir pour lire un fichier compressé de BackupPC.

Le code suivant de la bibliothèque flate2 permet de voir que flate2 a besoin, dans son constructeur, d'un BufRead pour lire un fichier compressé. Il utilise pour cela la méthode fill_buf pour remplir un tampon et la méthode consume pour consommer le tampon.

impl<R: Read> Read for BufReader<R> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // If we don't have any buffered data and we're doing a massive read
        // (larger than our internal buffer), bypass our internal buffer
        // entirely.
        if self.pos == self.cap && buf.len() >= self.buf.len() {
            return self.inner.read(buf);
        }
        let nread = {
            let mut rem = self.fill_buf()?;
            rem.read(buf)?
        };
        self.consume(nread);
        Ok(nread)
    }
}

J'ai donc commencé par écrire un adaptateur qui vient s'insérer entre le fichier et le décompresseur. Cet adaptateur a pour but de reproduire le comportement et de remplacer les octets en début de fichier.

struct InterpretAdapter<R: BufRead> {
    inner: R,
    first: bool,
    temp: Option<Vec<u8>>,
}
...
impl<R: BufRead> BufRead for InterpretAdapter<R> {
    fn fill_buf(&mut self) -> io::Result<&[u8]> {
        if self.temp.is_none() {
            let buf = self.inner.fill_buf()?;
            let mut buf = buf.to_vec();

            if self.first && !buf.is_empty() {
                self.first = false;

                if buf[0] == 0xd6 || buf[0] == 0xd7 {
                    buf[0] = 0x78;
                } else if buf[0] == 0xb3 {
                    // EOF
                    buf = Vec::new();
                }
            }

            self.temp = Some(buf);
        }

        Ok(self.temp.as_ref().unwrap())
    }

    fn consume(&mut self, amt: usize) {
        if amt > 0 {
            self.temp = None;
            self.inner.consume(amt);
        }
    }
}

Cet adaptateur vient s'insérer entre le BufReader et le ZLibDecoder. Du point de vue du ZLibDecoder, il doit se comporter comme un BufReader et donc répondre aux méthodes fill_buf et consume. Dans ces méthodes, je dois retourner un buffer. Je ne peux pas retourner le buffer du BufReader après une modification car ce dernier est une référence et est en
lecture seule. Je dois donc copier le contenu (je n'ai pas trouvé de meilleure manière de faire cela).

Ensuite, pour créer le Reader utilisé pour lire le fichier compressé, on boucle. On décompresse le contenu et si le décodeur retourne 0, on passe à un nouveau décodeur pour lire le morceau suivant.

pub struct BackupPCReader<R: Read> {
    decoder: Option<ZlibDecoder<InterpretAdapter<BufReader<R>>>>,
}

fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    loop {
        let decoder = self.decoder.as_mut();
        match decoder {
            None => return Ok(0),
            _ => {}
        }

        let decoder_read_result = decoder.unwrap().read(buf);

        let count = match decoder_read_result {
            Ok(count) => count
            Err(e) => {
                return Err(e);
            }
        };

        if count != 0 {
            return Ok(count);
        }

        if count == 0 {
            let decoder = self.decoder.take();
            match decoder {
                Some(decoder) => {
                    let mut reader = decoder.into_inner();
                    if reader.fill_buf()?.len() == 0 {
                        return Ok(0);
                    }
                    reader.reset();

                    self.decoder = Some(ZlibDecoder::new(reader));
                }
                None => {}
            }
        }
    }
}

Les fichiers d'attributs

Je me suis ensuite attelé au décodage des fichiers d'attributs. En réécrivant le code, j'ai découvert que certains nombres étaient mal encodés et donc causaient des erreurs de décodage, que j'ai dû gérer avec plus de souplesse.

Pour lire un Varint, j'ai pu ajouter un trait au Read:

pub trait VarintRead: Read {
    fn read_varint(&mut self) -> io::Result<u64> {
        let mut result = 0;
        let mut shift = 0;

        loop {
            let mut buf: [u8; 1] = [0u8; 1];
            self.read_exact(&mut buf)?;

            let byte = buf[0];
            let val = (byte & 0x7F) as u64;
            if shift >= 64 || val << shift >> shift != val {
                eprintln!("Varint too large: probably corrupted data");
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "Varint too large: probably corrupted data",
                ));
            }

            result |= val << shift;
            if byte & 0x80 == 0 {
                return Ok(result);
            }
            shift += 7;
        }
    }
}

Dans certains cas, je me retrouve donc avec le message d'erreur Varint too large: probably corrupted data. Quand j'ai ce message pour le fichier, je peux confirmer sur BackupPC que le fichier est également corrompu:

Corrupted file

BackupPC a aussi ce petit problème de décodage :) donc tout va bien. Mais par contre, la question est : est-ce que mes données sont corrompues ou est-ce que c'est un bug de BackupPC ?

Le driver fuse

L'étape suivante a été d'écrire un driver FUSE qui mélange tout cela. La lecture des fichiers d'attributs pour recréer la structure du système de fichier (qui sont dans le pool et compressés). Et la lecture des fichiers compressés pour les données.

Pour faire la partie du système de fichier, j'ai utilisé la librairie fuser. Le cœur ayant été écrit. La partie système de fichier consiste surtout à décoder le chemin fourni par FUSE, à lire le fichier d'attributs, et à lire le fichier compressé.

Pour faire ce décodage, dans le fichier view.rs,
j'ai ajouté des tests unitaires. Ces tests unitaires m'ont permis d'itérer plus rapidement pour construire le système de fichier.

Une fois le système de fichier construit, il m'a fallu le tester avec des tests réels. Le premier test a été de parcourir tout le système de fichier pour voir si la partie attribut fonctionnait bien. Pour cela, j'ai utilisé le programme filelight. Ce programme permet de parcourir le système de fichier et de voir la taille des fichiers.

Enfin, pour tester que j'étais capable de lire tous les fichiers du système de fichier, j'ai écrit un petit script sh.

#!/bin/bash

# Fichiers de sortie
output_file="md5sums.txt"
error_file="errors.txt"

# Parcourir tous les fichiers du système de fichiers
find /home/phoenix/tmp/test -type f -print0 | while IFS= read -r -d '' file; do
    # Essayer de calculer le hash MD5 du fichier
    if md5sum "$file" >> "$output_file"; then
        echo "Processed $file"
    else
        # Si le fichier ne peut pas être lu, écrire le nom du fichier dans le fichier d'erreur
        echo "Failed to process $file" >> "$error_file"
    fi
done

Développer avec Rust

Ce que j'ai apprécié avec Rust

En écrivant ce petit programme Rust, je me suis senti protégé. J’apprécie de pouvoir écrire du code bas niveau sans les soucis de devoir gérer la mémoire comme en C.

La documentation de Rust est bien faite et permet de progresser rapidement.

Je trouve que syntaxiquement le Rust est très lisible (tant qu'on n'utilise pas les durées de vie).

Ce que j'ai trouvé étrange avec Rust

L'idée de Rust est qu'à la compilation, on ne peut pas avoir de problème de concurrence, de fuite mémoire, de segmentation.

Mais en cherchant comment faire certaines opérations sur internet, je me retrouve avec du code contenant le mot unsafe. Je me suis refusé à l'utiliser mais pourquoi faire un langage qui se veut sûr et qui permet de faire des choses unsafe ?

De la même manière, j'ai l'impression que Rust nous oblige souvent à utiliser le clonage d'objet. Parfois j'aurais préféré ne pas cloner l'objet mais faire une référence (pour des questions de performances), mais là on se retrouve à gérer des durées de vie. Parfois je n'ai pas trouvé comment éviter le clonage simplement.

La syntaxe des durées de vie est complexe et rend le code peu lisible. Là aussi, je trouve dommage que Rust ne soit pas capable de gérer ces durées de vie tout seul.

Pour une partie du développement, j'ai fait des tests unitaires. L'écriture des tests unitaires n'a pas été simple (surtout quand on est habitué à des bibliothèques comme Jest en JavaScript).
Faire un mock des interfaces et des autres fichiers n'est pas quelque chose de simple et n'est pas natif à Rust. J'ai dû utiliser une bibliothèque dédiée (mockall).

Les mocks m'ont forcé à créer des structures alors qu'à l'origine j'avais des méthodes. Est-ce une bonne pratique de faire des structures pour tout ? Je suis parti sur des méthodes statiques mais la gestion des mocks sur les méthodes statiques ne permet pas de faire des tests unitaires en parallèle dans différents tests. Pour lancer les tests unitaires, je suis obligé de lancer un test à la fois:

RUST_TEST_THREADS=1 cargo test

De plus, je trouve étrange (par habitude) de devoir mettre les tests dans le même fichier que le code applicatif.

 Ma PR

L'idée de ce billet, c'est de vous proposer de relire mon code. De me dire ce que vous en pensez. En effet, débutant en Rust, j'aimerais prendre les bons réflexes dès le début. J'ai donc besoin de vos retours pour m'améliorer.

Voici le lien vers la PR : https://github.com/phoenix741/backuppc_pool_reader/pull/1.

N’hésitez pas à commenter directement sur la PR, critiquer le code et surtout conseiller sur comment l'améliorer. Si vous ne souhaitez pas commenter sur Github, le code se trouve aussi sur mon Gitea : https://gogs.shadoware.org/phoenix/backuppc_pool/pulls/1.

Autre question, si parmi vous il y a des personnes qui font des PR publiques et qui n'utilisent pas GitHub. Qu'utilisez-vous ?

Je me permets de publier ce billet sur mon blog en partie plus tard (les parties intéressantes :))

Commentaires : voir le flux Atom ouvrir dans le navigateur

par phoenix

LinuxFr.org : les journaux

LinuxFr.org : Journaux

Téléphone sous Linux ?

 -  25 avril - 

Aujourd'hui, avoir un téléphone avec un Android libéré, c'est possible, on pense en particulier à Murena.Avoir un téléphone sous GNU/Linux, c'est (...)


Quand votre voiture vous espionne… et vous le fait payer

 -  23 avril - 

Ceci se passe aux États-Unis, pour l’instant, aucune preuve qu’une telle fuite existe en Europe. Mais… si votre assurance augmente brutalement, (...)


firefox, nouvelle fenêtre dans une session isolée

 -  15 avril - 

Les fenêtres de navigation privées de firefox partagent leurs cookies de session or je souhaitais avoir des fenêtres de navigation isolées, (qui ne (...)


Pretendo tente de déprogrammer l'obsolescence des consoles Nintendo

 -  9 avril - 

Ah Nal,Gros N vient de faire un gros doigt aux utilisateurs de ses consoles 3DS et Wii U en annonçant la fermeture des services en ligne pour (...)


[Trolldi] Vulgarisation sur l'IA pour décideur pressé

 -  5 avril - 

Cher 'Nal,Je fais un article-marque-page sur un post tout frais de Ploum où il est question d'un fantasme vieux comme le Talmud avec le Golem. (...)