Chargement depuis un fichier

Nous souhaitons pouvoir charger nos listes de comptes depuis un fichier plutôt que les passer sur la ligne de commande. Cela permettra de traiter un grand nombre de comptes sans être limité par la taille maximale de la ligne de commande.

Au préalable, nous devons réaliser que nous allons avoir deux types d'erreur à propager :

  • les erreurs liées aux entrées-sorties, de type std::io::Error ;
  • les erreurs liées à l'analyse de la ligne "login:password", de type account::NoColon.

Étant donné que les structures Result<T, E> ne contiennent qu'un seul type d'erreur, de type E, nous avons deux choix d'implémentation possibles :

  • définir un nouveau type d'erreur qui nous est propre et qui peut encapsuler l'une ou l'autre de nos erreurs ;
  • implémenter le trait std::error::Error sur account::NoColon (qui est déjà implémenté pour std::io::Error) et utiliser le type dynamique Box<dyn std::error::Error> pour représenter une erreur générique.

Nous allons choisir la première méthode et définir un enum Error dans un module error de notre programme qui encapsulera les deux types d'erreurs possibles.

Création d'un type error::Error

  1. Créez un nouveau module error dans le projet.
  2. Définissez une énumération error::Error permettant d'encapsuler les deux formes d'erreur
#[derive(Debug)]
pub enum Error {
  IoError(std::io::Error),
  NoColon,
}
  1. Supprimez la définition du type vide account::NoColon, et remplacez son utilisation par error::Error dans les signatures de fonctions et par error::Error::NoColon pour signaler l'occurrence de l'erreur.

  2. Implémentez le trait From<std::io::Error> pour error::Error. Cela permettra à l'opérateur ? de convertir grâce à l'appel implicite à into() une erreur de type std::io::Error en error::Error.

Si vous le souhaitez, vous pouvez utiliser le crate thiserror pour implémenter plus facilement vos erreurs et des conversions automatiques. Cela nécessite d'en lire la documentation.

Chargement depuis un fichier

  1. Écrivez une méthode Account::from_file() permettant de charger un compte par ligne depuis un fichier, avec la signature suivante :
impl Account {
  …

  fn from_file(filename: &Path) -> Result<Vec<Account>, error::Error> {
    todo!()
  }
}

On pourra utilement regarder les fonctions Path::new(), File::open(), BufReader::new() qui permet d'encapsuler un descripteur de fichier dans un lecteur intelligent, le trait BufRead et notamment sa méthode lines() qui renvoie un itérateur sur les lignes d'un lecteur intelligent, ainsi que la méthode Result::map_err() qui permet de transformer une erreur en une autre.

On rappelera également que pour convertir une entité t de type T en type U, on pourra utiliser :

  • t.into() si le contexte indique qu'un type U est requis et que T implémente Into<U> ou que U implémente From<T>
  • U::from(t) si U implémente From<T>

Dit autrement, il sera possible au besoin de convertir une entité r contenant un Result<T, std::io::Error> en Result<T, error:Error> en appelant x.map_err(|e| error::Error::from(e)) ou de manière plus concise x.map_err(error::Error::from).

  1. Ajoutez une option --file=FILE (avec la version courte -f FILE) à la sous-commande group de votre programme pour charger la liste des comptes depuis le fichier FILE. Ce fichier sera optionnel:
struct AppArgs {
    …
    #[clap(short, long)]
    /// Load passwords from a file
    file: Option<PathBuf>,
}

Pour que notre programme fonctionne sur des systèmes de fichiers dont les noms ne sont pas codés en UTF-8 il faut prendre soin de ne pas mettre la valeur de l'argument dans une String mais dans un PathBuf. Ce type manipule des noms et chemins de fichiers qui ne sont pas nécessairement des chaînes UTF-8 valides.

  1. Indiquez que les comptes données sur la ligne de commande ne sont plus obligatoires en modifiant l'attribut #[clap] sur ce champ.

  2. Indiquez qu'un des deux paramètres file ou account sont obligatoires en ajoutant l'attribut idoine sur la structure AppArgs:

#[clap(group(
    ArgGroup::new("input")
        .required(true)
        .args(&["account", "file"]),
))]
struct AppArgs {
  …
}
  1. Vérifiez que votre programme se comporte bien quand vous précisez un nom de fichier, qu'il signale bien une erreur quand un mauvais nom est indiqué, qu'il rejette correctement une liste de comptes vide passée sur la ligne de commande.

Vous pouvez télécharger un fichier avec un grand nombre de comptes.