Comment ne pas réécrire un projet dans Rust

DĂšs que vous franchissez le seuil de douleur Borrow-Checker et rĂ©alisez que Rust vous permet de faire des choses inimaginables (et parfois dangereuses) dans d'autres langues, vous pouvez Ă©galement avoir le mĂȘme dĂ©sir irrĂ©sistible de tout rĂ©Ă©crire sur Rust . Bien que dans le meilleur des cas, cela soit tout Ă  fait improductif (efforts de dilapidation sans signification pour plusieurs projets), dans le pire des cas, cela entraĂźne une diminution de la qualitĂ© du code (aprĂšs tout, pourquoi vous considĂ©rez-vous plus expĂ©rimentĂ© dans le domaine de l'utilisation de la bibliothĂšque que son auteur d'origine?)


Il serait beaucoup plus utile de fournir une interface sécurisée pour la bibliothÚque d'origine en réutilisant son code.


‱ Premiers pas
‱ Nous collectons chmlib-sys
‱ Écriture d'un wrapper sĂ©curisĂ© dans Rust
‱ Rechercher des articles par nom
‱ Contourner les Ă©lĂ©ments par filtre
‱ Lecture du contenu des fichiers
‱ Ajouter des exemples
‱ Table des matiùres du fichier CHM
‱ DĂ©ballage du fichier CHM sur le disque
‱ Et ensuite?


Cet article présente un vrai projet. J'ai dû extraire des informations des fichiers CHM existants, mais je n'ai pas eu le temps de comprendre le format. La paresse est le moteur du progrÚs.

La caisse chmlib est publiée sur crates.io , et son code source est disponible sur GitHub . Si vous le trouvez utile ou rencontrez des problÚmes, faites-le moi savoir via le bugtracker .

Premiers pas


Pour commencer, il vaut la peine de comprendre comment le travail avec la bibliothÚque a été initialement conçu.


Cela vous apprendra non seulement Ă  l'utiliser, mais vous assurera Ă©galement que tout se passe. Si vous avez de la chance, vous trouverez mĂȘme des tests et des exemples prĂȘts Ă  l'emploi.

Ne sautez pas cette Ă©tape!

Nous travaillerons avec CHMLib , une bibliothĂšque C pour lire les fichiers Microsoft Compiled HTML Help ( .chm ).


Commençons par créer un nouveau projet et connecter CHMLib en tant que sous-module git:


 $ git init chmlib && cd chmlib Initialized empty Git repository in /home/michael/Documents/chmlib/.git/ $ touch README.md Cargo.toml $ cargo new --lib chmlib Created library `chmlib` package $ cargo new --lib chmlib-sys Created library `chmlib-sys` package $ cat Cargo.toml [workspace] members = ["chmlib", "chmlib-sys"] $ git submodule add git@github.com:jedwing/CHMLib.git vendor/CHMLib Cloning into '/home/michael/Documents/chmlib/vendor/CHMLib'... remote: Enumerating objects: 99, done. remote: Total 99 (delta 0), reused 0 (delta 0), pack-reused 99 Receiving objects: 100% (99/99), 375.51 KiB | 430.00 KiB/s, done. Resolving deltas: 100% (45/45), done. 

AprĂšs cela, jetez un Ɠil Ă  ce qui se trouve Ă  l'intĂ©rieur de l' tree :


 $ tree vendor/CHMLib vendor/CHMLib ├── acinclude.m4 ├── AUTHORS ├── ChangeLog ├── ChmLib-ce.zip ├── ChmLib-ds6.zip ├── configure.in ├── contrib │ └── mozilla_helper.sh ├── COPYING ├── Makefile.am ├── NEWS ├── NOTES ├── README └── src ├── chm_http.c ├── chm_lib.c ├── chm_lib.h ├── enum_chmLib.c ├── enumdir_chmLib.c ├── extract_chmLib.c ├── lzx.c ├── lzx.h ├── Makefile.am ├── Makefile.simple └── test_chmLib.c 2 directories, 23 files 

Il semble que la bibliothĂšque utilise GNU Autotools pour construire. Ce n'est pas bon, car tous les utilisateurs de la caisse chmlib (et leurs utilisateurs) devront installer Autotools.


Nous allons essayer de nous débarrasser de cette dépendance "contagieuse" en collectant le code C manuellement, mais plus à ce sujet plus tard.

Les fichiers lzx.h et lzx.c contiennent une implĂ©mentation de l'algorithme de compression LZX . En gĂ©nĂ©ral, il serait prĂ©fĂ©rable d'utiliser une sorte de bibliothĂšque liblzx pour obtenir des mises Ă  jour gratuites et tout ça, mais il serait peut-ĂȘtre plus facile de compiler bĂȘtement ces fichiers.


enum_chmLib.c, enumdir_chmLib.c, extract_chmLib.c semblent ĂȘtre des exemples d'utilisation des fonctions chm_enumerate (), chm_enumerate_dir (), chm_retrieve_object (). Cela vous sera utile ...


Le fichier test_chmLib.c contient un autre exemple, cette fois en extrayant une page du fichier CHM sur le disque.


chm_http.c implémente un simple serveur HTTP affichant un fichier .chm dans un navigateur. Cela ne sera probablement plus utile.


Nous avons donc trié tout ce qui se trouve dans le fournisseur / CHMLib / src. Allons-nous récupérer la bibliothÚque?


HonnĂȘtement, il est assez petit pour appliquer la mĂ©thode scientifique du poke.


 $ clang chm_lib.c enum_chmLib.c -o enum_chmLib /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `chm_close': chm_lib.c:(.text+0x8fa): undefined reference to `LZXteardown' /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_region': chm_lib.c:(.text+0x18ca): undefined reference to `LZXinit' /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_block': chm_lib.c:(.text+0x2900): undefined reference to `LZXreset' /usr/bin/ld: chm_lib.c:(.text+0x2a4b): undefined reference to `LZXdecompress' /usr/bin/ld: chm_lib.c:(.text+0x2abe): undefined reference to `LZXreset' /usr/bin/ld: chm_lib.c:(.text+0x2bf4): undefined reference to `LZXdecompress' clang: error: linker command failed with exit code 1 (use -v to see invocation) 

D'accord, peut-ĂȘtre que ce LZX est toujours nĂ©cessaire ...


 $ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib 

Euh ... tout?


Pour m'assurer que le code fonctionne, j'ai téléchargé un exemple sur Internet:


 $ curl http://www.innovasys.com/static/hs/samples/topics.classic.chm.zip \ -o topics.classic.chm.zip $ unzip topics.classic.chm.zip Archive: topics.classic.chm.zip inflating: output/compiled/topics.classic.chm $ file output/compiled/topics.classic.chm output/compiled/topics.classic.chm: MS Windows HtmlHelp Data 

Voyons comment enum_chmLib le gĂšre:


 $ ./enum_chmLib output/compiled/topics.classic.chm output/compiled/topics.classic.chm: spc start length type name === ===== ====== ==== ==== 0 0 0 normal dir / 1 5125797 4096 special file /#IDXHDR ... 1 4944434 11234 normal file /BrowserView.html ... 0 0 0 normal dir /flash/ 1 532689 727 normal file /flash/expressinstall.swf 0 0 0 normal dir /Images/Commands/RealWorld/ 1 24363 1254 normal file /Images/Commands/RealWorld/BrowserBack.bmp ... 1 35672 1021 normal file /Images/Employees24.gif ... 1 3630715 200143 normal file /template/packages/jquery-mobile/script/ jquery.mobile-1.4.5.min.js ... 0 134 1296 meta file ::DataSpace/Storage/MSCompressed/Transform/ {7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/ InstanceData/ResetTable 

Seigneur, mĂȘme ici jQuery ÂŻ \ _ (ツ) _ / ÂŻ


Construire chmlib-sys


Nous en savons maintenant assez pour utiliser CHMLib dans la caisse chmlib -sys , qui est responsable de la construction de la bibliothĂšque native, de sa liaison avec le compilateur Rast et d'une interface avec les fonctions C.


Pour crĂ©er la bibliothĂšque, vous devez Ă©crire le fichier build.rs . À l'aide de la caisse cc , il appellera le compilateur C et fera d'autres amitiĂ©s pour que tout fonctionne ensemble comme il se doit.


Nous avons la chance de pouvoir déplacer la majeure partie du travail en cc, mais parfois c'est beaucoup plus difficile. En savoir plus dans la documentation des scripts d'assemblage .

Ajoutez d'abord cc comme dépendance pour chmlib-sys:


 $ cd chmlib-sys $ cargo add --build cc Updating 'https://github.com/rust-lang/crates.io-index' index Adding cc v1.0.46 to build-dependencies 

Ensuite, nous Ă©crivons build.rs :


 // chmlib-sys/build.rs use cc::Build; use std::{env, path::PathBuf}; fn main() { let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) .canonicalize() .unwrap(); let root_dir = project_dir.parent().unwrap(); let src = root_dir.join("vendor").join("CHMLib").join("src"); Build::new() .file(src.join("chm_lib.c")) .file(src.join("lzx.c")) .include(&src) .warnings(false) .compile("chmlib"); } 

Vous devez également indiquer à Cargo que chmlib-sys est lié à la bibliothÚque chmlib. Cargo peut alors garantir que dans le graphique de dépendance entier, il n'y a qu'un seul rack, en fonction de la bibliothÚque native spécifique. Cela évite les messages d'erreur obscurs sur les caractÚres répétés ou l'utilisation accidentelle de bibliothÚques incompatibles.


 --- a/chmlib-sys/Cargo.toml +++ b/chmlib-sys/Cargo.toml @@ -3,7 +3,13 @@ name = "chmlib-sys" version = "0.1.0" authors = ["Michael Bryan <michaelfbryan@gmail.com>"] edition = "2018" description = "Raw bindings to the CHMLib C library" license = "LGPL" repository = "https://github.com/Michael-F-Bryan/chmlib" +links = "chmlib" +build = "build.rs" [dependencies] [build-dependencies] cc = { version = "1.0" } 

Ensuite, nous devons dĂ©clarer toutes les fonctions exportĂ©es par la bibliothĂšque chmlib afin qu'elles puissent ĂȘtre utilisĂ©es Ă  partir de Rast.


C'est pour cela qu'il y a un merveilleux projet bindgen . Le fichier d'en-tĂȘte C est donnĂ© Ă  l'entrĂ©e et le fichier avec les liaisons FFI pour Rast est sorti.


 $ cargo install bindgen $ bindgen ../vendor/CHMLib/src/chm_lib.h \ -o src/lib.rs \ --raw-line '#![allow(non_snake_case, non_camel_case_types)]' $ head src/lib.rs /* automatically generated by rust-bindgen */ #![allow(non_snake_case, non_camel_case_types)] pub const CHM_UNCOMPRESSED: u32 = 0; pub const CHM_COMPRESSED: u32 = 1; pub const CHM_MAX_PATHLEN: u32 = 512; pub const CHM_PARAM_MAX_BLOCKS_CACHED: u32 = 0; pub const CHM_RESOLVE_SUCCESS: u32 = 0; pub const CHM_RESOLVE_FAILURE: u32 = 1; $ tail src/lib.rs extern "C" { pub fn chm_enumerate_dir( h: *mut chmFile, prefix: *const ::std::os::raw::c_char, what: ::std::os::raw::c_int, e: CHM_ENUMERATOR, context: *mut ::std::os::raw::c_void, ) -> ::std::os::raw::c_int; } 

Je recommande fortement de lire le manuel d'utilisation de Bindgen si vous avez besoin de réparer quelque chose dans son échappement.

À ce stade, il sera utile d'Ă©crire un test de fumĂ©e qui vĂ©rifiera que tout fonctionne comme prĂ©vu et que nous pouvons rĂ©ellement appeler les fonctions de la bibliothĂšque C d'origine.


 // chmlib-sys/tests/smoke_test.rs //    Path  char*     . //  , OsStr ( Path)  Windows  [u16]  , //        char*. #![cfg(unix)] use std::{ffi::CString, os::unix::ffi::OsStrExt, path::Path}; #[test] fn open_example_file() { let project_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let sample_chm = project_dir.parent().unwrap().join("topics.classic.chm"); let c_str = CString::new(sample_chm.as_os_str().as_bytes()).unwrap(); unsafe { let handle = chmlib_sys::chm_open(c_str.as_ptr()); assert!(!handle.is_null()); chmlib_sys::chm_close(handle); } } 

cargo test dit que tout semble ĂȘtre en ordre:


 $ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.03s Running ~/chmlib/target/debug/deps/chmlib_sys-2ffd7b11a9fd8437 running 1 test test bindgen_test_layout_chmUnitInfo ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running ~/chmlib/target/debug/deps/smoke_test-f7be9810412559dc running 1 test test open_example_file ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests chmlib-sys running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 

Écrire un wrapper sĂ©curisĂ© dans Rust


Techniquement et techniquement, nous pouvons maintenant appeler CHMLib depuis Rasta, mais cela nécessite un tas dangereux . Cela peut fonctionner pour un métier hackneyed, mais pour la publication sur crates.io, il vaut la peine d'écrire un wrapper sécurisé pour tout le code dangereux.


Si vous regardez l'API chmlib-sys en utilisant cargo doc --open , vous pouvez voir de nombreuses fonctions qui prennent *mut ChmFile comme premier argument. Ceci est similaire aux objets et aux méthodes.


Fichier d'en-tĂȘte CHMLib
 /* $Id: chm_lib.h,v 1.10 2002/10/09 01:16:33 jedwin Exp $ */ /*************************************************************************** * chm_lib.h - CHM archive manipulation routines * * ------------------- * * * * author: Jed Wing <jedwin@ugcs.caltech.edu> * * version: 0.3 * * notes: These routines are meant for the manipulation of microsoft * * .chm (compiled html help) files, but may likely be used * * for the manipulation of any ITSS archive, if ever ITSS * * archives are used for any other purpose. * * * * Note also that the section names are statically handled. * * To be entirely correct, the section names should be read * * from the section names meta-file, and then the various * * content sections and the "transforms" to apply to the data * * they contain should be inferred from the section name and * * the meta-files referenced using that name; however, all of * * the files I've been able to get my hands on appear to have * * only two sections: Uncompressed and MSCompressed. * * Additionally, the ITSS.DLL file included with Windows does * * not appear to handle any different transforms than the * * simple LZX-transform. Furthermore, the list of transforms * * to apply is broken, in that only half the required space * * is allocated for the list. (It appears as though the * * space is allocated for ASCII strings, but the strings are * * written as unicode. As a result, only the first half of * * the string appears.) So this is probably not too big of * * a deal, at least until CHM v4 (MS .lit files), which also * * incorporate encryption, of some description. * ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation; either version 2.1 of the * * License, or (at your option) any later version. * * * ***************************************************************************/ #ifndef INCLUDED_CHMLIB_H #define INCLUDED_CHMLIB_H #ifdef __cplusplus extern "C" { #endif /* RWE 6/12/1002 */ #ifdef PPC_BSTR #include <wtypes.h> #endif #ifdef WIN32 #ifdef __MINGW32__ #define __int64 long long #endif typedef unsigned __int64 LONGUINT64; typedef __int64 LONGINT64; #else typedef unsigned long long LONGUINT64; typedef long long LONGINT64; #endif /* the two available spaces in a CHM file */ /* NB: The format supports arbitrarily many spaces, but only */ /* two appear to be used at present. */ #define CHM_UNCOMPRESSED (0) #define CHM_COMPRESSED (1) /* structure representing an ITS (CHM) file stream */ struct chmFile; /* structure representing an element from an ITS file stream */ #define CHM_MAX_PATHLEN (512) struct chmUnitInfo { LONGUINT64 start; LONGUINT64 length; int space; int flags; char path[CHM_MAX_PATHLEN+1]; }; /* open an ITS archive */ #ifdef PPC_BSTR /* RWE 6/12/2003 */ struct chmFile* chm_open(BSTR filename); #else struct chmFile* chm_open(const char *filename); #endif /* close an ITS archive */ void chm_close(struct chmFile *h); /* methods for ssetting tuning parameters for particular file */ #define CHM_PARAM_MAX_BLOCKS_CACHED 0 void chm_set_param(struct chmFile *h, int paramType, int paramVal); /* resolve a particular object from the archive */ #define CHM_RESOLVE_SUCCESS (0) #define CHM_RESOLVE_FAILURE (1) int chm_resolve_object(struct chmFile *h, const char *objPath, struct chmUnitInfo *ui); /* retrieve part of an object from the archive */ LONGINT64 chm_retrieve_object(struct chmFile *h, struct chmUnitInfo *ui, unsigned char *buf, LONGUINT64 addr, LONGINT64 len); /* enumerate the objects in the .chm archive */ typedef int (*CHM_ENUMERATOR)(struct chmFile *h, struct chmUnitInfo *ui, void *context); #define CHM_ENUMERATE_NORMAL (1) #define CHM_ENUMERATE_META (2) #define CHM_ENUMERATE_SPECIAL (4) #define CHM_ENUMERATE_FILES (8) #define CHM_ENUMERATE_DIRS (16) #define CHM_ENUMERATE_ALL (31) #define CHM_ENUMERATOR_FAILURE (0) #define CHM_ENUMERATOR_CONTINUE (1) #define CHM_ENUMERATOR_SUCCESS (2) int chm_enumerate(struct chmFile *h, int what, CHM_ENUMERATOR e, void *context); int chm_enumerate_dir(struct chmFile *h, const char *prefix, int what, CHM_ENUMERATOR e, void *context); #ifdef __cplusplus } #endif #endif /* INCLUDED_CHMLIB_H */ 

Commençons par le type de données, qui appelle chm_open () dans le constructeur et chm_close () dans le destructeur.


 pub unsafe extern "C" fn chm_open(filename: *const c_char) -> *mut chmFile; pub unsafe extern "C" fn chm_close(h: *mut chmFile); 

Pour simplifier la gestion des erreurs, nous utilisons la caisse thiserror , que std::error::Error implémente automatiquement.


 $ cd chmlib $ cargo add thiserror 

Maintenant, vous devez comprendre comment transformer std::path::Path en *const c_char . Malheureusement, ce n'est pas si facile à faire en raison de diverses blagues avec compatibilité .


 // chmlib/src/lib.rs use thiserror::Error; use std::{ffi::CString, path::Path}; #[cfg(unix)] fn path_to_cstring(path: &Path) -> Result<CString, InvalidPath> { use std::os::unix::ffi::OsStrExt; let bytes = path.as_os_str().as_bytes(); CString::new(bytes).map_err(|_| InvalidPath) } #[cfg(not(unix))] fn path_to_cstring(path: &Path) -> Result<CString, InvalidPath> { //  ,  Windows CHMLib  CreateFileA(),   //       ASCII.   ...   // ,          ? let rust_str = path.as_os_str().as_str().ok_or(InvalidPath)?; CString::new(rust_str).map_err(|_| InvalidPath) } #[derive(Error, Debug, Copy, Clone, PartialEq)] #[error("Invalid Path")] pub struct InvalidPath; 

DĂ©finissez maintenant la structure du ChmFile . Il stocke un pointeur non nul sur chmlib_sys :: chmFile. Si chm_open () renvoie un pointeur nul, cela signifie qu'elle n'a pas pu ouvrir le fichier en raison d'une sorte d'erreur.


 // chmlib/src/lib.rs use std::{ffi::CString, path::Path, ptr::NonNull}; #[derive(Debug)] pub struct ChmFile { raw: NonNull<chmlib_sys::chmFile>, } impl ChmFile { pub fn open<P: AsRef<Path>>(path: P) -> Result<ChmFile, OpenError> { let c_path = path_to_cstring(path.as_ref())?; // ,   c_path  unsafe { let raw = chmlib_sys::chm_open(c_path.as_ptr()); match NonNull::new(raw) { Some(raw) => Ok(ChmFile { raw }), None => Err(OpenError::Other), } } } } impl Drop for ChmFile { fn drop(&mut self) { unsafe { chmlib_sys::chm_close(self.raw.as_ptr()); } } } /// The error returned when we are unable to open a [`ChmFile`]. #[derive(Error, Debug, Copy, Clone, PartialEq)] pub enum OpenError { #[error("Invalid path")] InvalidPath(#[from] InvalidPath), #[error("Unable to open the ChmFile")] Other, } 

Pour vous assurer qu'il n'y a pas de fuite de mémoire, exécutez un test simple sous Valgrind . Il créera un ChmFile et le publiera immédiatement.


 // chmlib/src/lib.rs #[test] fn open_valid_chm_file() { let sample = sample_path(); //   let chm_file = ChmFile::open(&sample).unwrap(); //      drop(chm_file); } fn sample_path() -> PathBuf { let project_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let sample = project_dir.parent().unwrap().join("topics.classic.chm"); assert!(sample.exists()); sample } 

Valgrind dit qu'il n'y a plus de mémoire non comptabilisée:


 $ valgrind ../target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file ==8953== Memcheck, a memory error detector ==8953== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==8953== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info ==8953== Command: ~/chmlib/target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file ==8953== running 1 test test tests::open_valid_chm_file ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ==8953== ==8953== HEAP SUMMARY: ==8953== in use at exit: 0 bytes in 0 blocks ==8953== total heap usage: 249 allocs, 249 frees, 43,273 bytes allocated ==8953== ==8953== All heap blocks were freed -- no leaks are possible ==8953== ==8953== For counts of detected and suppressed errors, rerun with: -v ==8953== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) 

Rechercher des éléments par nom


Ensuite, la fonction chm_resolve_object ():


 pub const CHM_RESOLVE_SUCCESS: u32 = 0; pub const CHM_RESOLVE_FAILURE: u32 = 1; /* resolve a particular object from the archive */ pub unsafe extern "C" fn chm_resolve_object( h: *mut chmFile, objPath: *const c_char, ui: *mut chmUnitInfo ) -> c_int; 

La recherche peut échouer, donc chm_resolve_object () renvoie un code d'erreur signalant le succÚs ou l'échec, et les informations sur l'objet trouvé seront enregistrées par le pointeur transmis à chmUnitInfo .


Le type std::mem::MaybeUninit créé juste pour notre cas avec le paramÚtre out ui .


Pour l'instant, laissons la structure UnitInfo vide - c'est l'Ă©quivalent Rust de la structure chmUnitInfo C. Nous ajouterons les champs lorsque nous commencerons Ă  lire Ă  partir de ChmFile.


 // chmlib/src/lib.rs impl ChmFile { ... /// Find a particular object in the archive. pub fn find<P: AsRef<Path>>(&mut self, path: P) -> Option<UnitInfo> { let path = path_to_cstring(path.as_ref()).ok()?; unsafe { //   chmUnitInfo   let mut resolved = MaybeUninit::<chmlib_sys::chmUnitInfo>::uninit(); //  -  let ret = chmlib_sys::chm_resolve_object( self.raw.as_ptr(), path.as_ptr(), resolved.as_mut_ptr(), ); if ret == chmlib_sys::CHM_RESOLVE_SUCCESS { //    "resolved"   Some(UnitInfo::from_raw(resolved.assume_init())) } else { None } } } } #[derive(Debug)] pub struct UnitInfo; impl UnitInfo { fn from_raw(ui: chmlib_sys::chmUnitInfo) -> UnitInfo { UnitInfo } } 

Notez que ChmFile :: find () accepte &mut self , bien que le code sur le Rast ne contienne pas de changement d'état explicite. Le fait est que l'implémentation C utilise toutes sortes de fseek () pour se déplacer dans le fichier, donc l'état interne change toujours pendant la recherche.

Vérifions ChmFile :: find () sur le fichier expérimental que nous avons précédemment téléchargé:


 // chmlib/src/lib.rs #[test] fn find_an_item_in_the_sample() { let sample = sample_path(); let chm = ChmFile::open(&sample).unwrap(); assert!(chm.find("/BrowserView.html").is_some()); assert!(chm.find("doesn't exist.txt").is_none()); } 

Filtrer les éléments de contournement


CHMLib fournit une API pour afficher le contenu d'un fichier CHM via un filtre de masque de bits.


Prenez la caisse de bitflags pratique pour travailler avec des masques et des drapeaux:


 $ cargo add bitflags Updating 'https://github.com/rust-lang/crates.io-index' index Adding bitflags v1.2.1 to dependencies 

Et définissez les cases à cocher Filtrer en fonction des constantes de chm_lib.h:


 // chmlib/src/lib.rs bitflags::bitflags! { pub struct Filter: c_int { /// A normal file. const NORMAL = chmlib_sys::CHM_ENUMERATE_NORMAL as c_int; /// A meta file (typically used by the CHM system). const META = chmlib_sys::CHM_ENUMERATE_META as c_int; /// A special file (starts with `#` or `$`). const SPECIAL = chmlib_sys::CHM_ENUMERATE_SPECIAL as c_int; /// It's a file. const FILES = chmlib_sys::CHM_ENUMERATE_FILES as c_int; /// It's a directory. const DIRS = chmlib_sys::CHM_ENUMERATE_DIRS as c_int; } } 

Nous avons Ă©galement besoin d'un adaptateur extern "C" pour les fermetures Rastovyh, qui peut ĂȘtre passĂ© Ă  C sous la forme d'un pointeur vers une fonction:


 // chmlib/src/lib.rs unsafe extern "C" fn function_wrapper<F>( file: *mut chmlib_sys::chmFile, unit: *mut chmlib_sys::chmUnitInfo, state: *mut c_void, ) -> c_int where F: FnMut(&mut ChmFile, UnitInfo) -> Continuation, { //      FFI- let result = panic::catch_unwind(|| { //   ManuallyDrop    `&mut ChmFile` //        ( double-free). let mut file = ManuallyDrop::new(ChmFile { raw: NonNull::new_unchecked(file), }); let unit = UnitInfo::from_raw(unit.read()); //  state      let closure = &mut *(state as *mut F); closure(&mut file, unit) }); match result { Ok(Continuation::Continue) => { chmlib_sys::CHM_ENUMERATOR_CONTINUE as c_int }, Ok(Continuation::Stop) => chmlib_sys::CHM_ENUMERATOR_SUCCESS as c_int, Err(_) => chmlib_sys::CHM_ENUMERATOR_FAILURE as c_int, } } 

function_wrapper contient un code dangereux et délicat à utiliser:

  • Le pointeur d' state doit pointer vers l'instance de fermeture F.
  • Le code Rasta exĂ©cutĂ© par une fermeture peut provoquer la panique. Il ne doit pas traverser la frontiĂšre entre Rast et C, car la promotion de la pile dans diffĂ©rentes langues est un comportement non dĂ©fini. Une Ă©ventuelle panique doit ĂȘtre interceptĂ©e Ă  l'aide de std::panic::catch_unwind() .
  • Un pointeur vers chmlib_sys :: chmFile passĂ© Ă  function_wrapper est Ă©galement stockĂ© dans le ChmFile appelant. Pendant la durĂ©e de l'appel, vous devez vous assurer que seule la fermeture peut manipuler chmlib_sys :: chmFile, sinon une condition de concurrence critique peut se produire.
  • La fermeture doit ĂȘtre passĂ©e &mut ChmFile , et pour cela, vous devrez crĂ©er un objet temporaire sur la pile en utilisant le pointeur existant. Cependant, si le destructeur ChmFile s'exĂ©cute dans ce cas, alors chmlib_sys :: chmFile sera libĂ©rĂ© trop tĂŽt. Pour rĂ©soudre ce problĂšme, il existe std::mem::ManuallyDrop .

Voici comment function_wrapper est utilisé pour implémenter ChmFile::for_each() :


 // chmlib/src/lib.rs impl ChmFile { ... /// Inspect each item within the [`ChmFile`]. pub fn for_each<F>(&mut self, filter: Filter, mut cb: F) where F: FnMut(&mut ChmFile, UnitInfo) -> Continuation, { unsafe { chmlib_sys::chm_enumerate( self.raw.as_ptr(), filter.bits(), Some(function_wrapper::<F>), &mut cb as *mut _ as *mut c_void, ); } } /// Inspect each item within the [`ChmFile`] inside a specified directory. pub fn for_each_item_in_dir<F, P>( &mut self, filter: Filter, prefix: P, mut cb: F, ) where P: AsRef<Path>, F: FnMut(&mut ChmFile, UnitInfo) -> Continuation, { let path = match path_to_cstring(prefix.as_ref()) { Ok(p) => p, Err(_) => return, }; unsafe { chmlib_sys::chm_enumerate_dir( self.raw.as_ptr(), path.as_ptr(), filter.bits(), Some(function_wrapper::<F>), &mut cb as *mut _ as *mut c_void, ); } } } 

Remarquez comment le paramÚtre F interagit avec la fonction générique function_wrapper. Cette technique est souvent utilisée lorsque vous devez passer la fermeture Rust via FFI pour coder dans une autre langue.

Lecture du contenu du fichier


La derniĂšre fonction dont nous avons besoin est responsable de la lecture du fichier Ă  l'aide de chm_retrieve_object ().


Son implémentation est assez banale. Ceci est similaire à un trait std :: io :: Read typique, à l'exception d'un décalage de fichier explicite.


 // chmlib/src/lib.rs impl ChmFile { ... pub fn read( &mut self, unit: &UnitInfo, offset: u64, buffer: &mut [u8], ) -> Result<usize, ReadError> { let mut unit = unit.0.clone(); let bytes_written = unsafe { chmlib_sys::chm_retrieve_object( self.raw.as_ptr(), &mut unit, buffer.as_mut_ptr(), offset, buffer.len() as _, ) }; if bytes_written >= 0 { Ok(bytes_written as usize) } else { Err(ReadError) } } } #[derive(Error, Debug, Copy, Clone, PartialEq)] #[error("The read failed")] pub struct ReadError; 

Bien sûr, il serait bien d'avoir un message d'erreur plus détaillé que «échec de lecture», mais à en juger par le code source, chm_retrieve_object () ne fait pas de distinction particuliÚre entre les erreurs:


  • renvoie 0 lorsque le fichier est lu jusqu'Ă  la fin;
  • renvoie 0 pour les arguments invalides: pointeurs nuls ou dĂ©passement des limites;
  • renvoie -1 si le systĂšme ne lit pas correctement les fichiers (et remplit errno);
  • renvoie -1 pour les erreurs de dĂ©compression, sans distinguer la corruption des donnĂ©es et, par exemple, l'impossibilitĂ© d'allouer de la mĂ©moire pour un tampon temporaire via malloc ().

Vous pouvez tester ChmFile :: read () en utilisant des fichiers au contenu connu:


 // chmlib/src/lib.rs #[test] fn read_an_item() { let sample = sample_path(); let mut chm = ChmFile::open(&sample).unwrap(); let filename = "/template/packages/core-web/css/index.responsive.css"; //        let item = chm.find(filename).unwrap(); //      let mut buffer = vec![0; item.length() as usize]; let bytes_written = chm.read(&item, 0, &mut buffer).unwrap(); //      assert_eq!(bytes_written, item.length() as usize); // ...  ,    let got = String::from_utf8(buffer).unwrap(); assert!(got.starts_with( "html, body, div#i-index-container, div#i-index-body" )); } 

Ajouter des exemples


Nous avons couvert la plupart des API de la bibliothĂšque CHMLib et beaucoup en auraient fini avec cela, considĂ©rant que le portage s'est terminĂ© avec succĂšs. Cependant, ce serait bien de rendre notre rack encore plus convivial. — , Rust Go ( , rustdoc godoc ).


, CHMLib , .


, , .


CHM-


CHM- .


 /* $Id: enum_chmLib.c,v 1.7 2002/10/09 12:38:12 jedwin Exp $ */ /*************************************************************************** * enum_chmLib.c - CHM archive test driver * * ------------------- * * * * author: Jed Wing <jedwin@ugcs.caltech.edu> * * notes: This is a quick-and-dirty test driver for the chm lib * * routines. The program takes as its input the paths to one * * or more .chm files. It attempts to open each .chm file in * * turn, and display a listing of all of the files in the * * archive. * * * * It is not included as a particularly useful program, but * * rather as a sort of "simplest possible" example of how to * * use the enumerate portion of the API. * ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation; either version 2.1 of the * * License, or (at your option) any later version. * * * ***************************************************************************/ #include "chm_lib.h" #include <stdio.h> #include <stdlib.h> #include <string.h> /* * callback function for enumerate API */ int _print_ui(struct chmFile *h, struct chmUnitInfo *ui, void *context) { static char szBuf[128]; memset(szBuf, 0, 128); if(ui->flags & CHM_ENUMERATE_NORMAL) strcpy(szBuf, "normal "); else if(ui->flags & CHM_ENUMERATE_SPECIAL) strcpy(szBuf, "special "); else if(ui->flags & CHM_ENUMERATE_META) strcpy(szBuf, "meta "); if(ui->flags & CHM_ENUMERATE_DIRS) strcat(szBuf, "dir"); else if(ui->flags & CHM_ENUMERATE_FILES) strcat(szBuf, "file"); printf(" %1d %8d %8d %s\t\t%s\n", (int)ui->space, (int)ui->start, (int)ui->length, szBuf, ui->path); return CHM_ENUMERATOR_CONTINUE; } int main(int c, char **v) { struct chmFile *h; int i; for (i=1; i<c; i++) { h = chm_open(v[i]); if (h == NULL) { fprintf(stderr, "failed to open %s\n", v[i]); exit(1); } printf("%s:\n", v[i]); printf(" spc start length type\t\t\tname\n"); printf(" === ===== ====== ====\t\t\t====\n"); if (! chm_enumerate(h, CHM_ENUMERATE_ALL, _print_ui, NULL)) printf(" *** ERROR ***\n"); chm_close(h); } return 0; } 

_print_ui() Rust. UnitInfo , , .


 // chmlib/examples/enumerate-items.rs fn describe_item(item: UnitInfo) { let mut description = String::new(); if item.is_normal() { description.push_str("normal "); } else if item.is_special() { description.push_str("special "); } else if item.is_meta() { description.push_str("meta "); } if item.is_dir() { description.push_str("dir"); } else if item.is_file() { description.push_str("file"); } println!( " {} {:8} {:8} {}\t\t{}", item.space(), item.start(), item.length(), description, item.path().unwrap_or(Path::new("")).display() ); } 

main() , , describe_item() ChmFile::for_each().


 // chmlib/examples/enumerate-items.rs fn main() { let filename = env::args() .nth(1) .unwrap_or_else(|| panic!("Usage: enumerate-items <filename>")); let mut file = ChmFile::open(&filename).expect("Unable to open the file"); println!("{}:", filename); println!(" spc start length type\t\t\tname"); println!(" === ===== ====== ====\t\t\t===="); file.for_each(Filter::all(), |_file, item| { describe_item(item); Continuation::Continue }); } 

:


 $ cargo run --example enumerate-items topics.classic.chm > rust-example.txt $ cd vendor/CHMLib/src $ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib $ cd ../../.. $ ./vendor/CHMLib/src/enum_chmLib topics.classic.chm > c-example.txt $ diff -u rust-example.txt c-example.txt $ echo $? 0 

diff , , , , . - , diff.


 diff --git a/chmlib/examples/enumerate-items.rs b/chmlib/examples/enumerate-items.rs index e68fa58..ef855ac 100644 --- a/chmlib/examples/enumerate-items.rs +++ b/chmlib/examples/enumerate-items.rs @@ -36,6 +36,10 @@ fn describe_item(item: UnitInfo) { description.push_str("file"); } + if item.length() % 7 == 0 { + description.push_str(" :)"); + } + println!( " {} {:8} {:8} {}\t\t{}", item.space(), 

:


 $ cargo run --example enumerate-items topics.classic.chm > rust-example.txt $ diff -u rust-example.txt c-example.txt --- rust-example.txt 2019-10-20 16:51:53.933560892 +0800 +++ c-example.txt 2019-10-20 16:40:42.007053966 +0800 @@ -1,9 +1,9 @@ topics.classic.chm: spc start length type name === ===== ====== ==== ==== - 0 0 0 normal dir :) / + 0 0 0 normal dir / 1 5125797 4096 special file /#IDXHDR - 0 0 0 special file :) /#ITBITS + 0 0 0 special file /#ITBITS 1 5104520 148 special file /#IVB 1 5132009 1227 special file /#STRINGS 0 1430 4283 special file /#SYSTEM @@ -13,9 +13,9 @@ ... 

!


CHM-


, CHMLib, «» .


 /* $Id: extract_chmLib.c,v 1.4 2002/10/10 03:24:51 jedwin Exp $ */ /*************************************************************************** * extract_chmLib.c - CHM archive extractor * * ------------------- * * * * author: Jed Wing <jedwin@ugcs.caltech.edu> * * notes: This is a quick-and-dirty chm archive extractor. * ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation; either version 2.1 of the * * License, or (at your option) any later version. * * * ***************************************************************************/ #include "chm_lib.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef WIN32 #include <windows.h> #include <direct.h> #define mkdir(X, Y) _mkdir(X) #define snprintf _snprintf #else #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #endif struct extract_context { const char *base_path; }; static int dir_exists(const char *path) { #ifdef WIN32 /* why doesn't this work?!? */ HANDLE hFile; hFile = CreateFileA(path, FILE_LIST_DIRECTORY, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { CloseHandle(hFile); return 1; } else return 0; #else struct stat statbuf; if (stat(path, &statbuf) != -1) return 1; else return 0; #endif } static int rmkdir(char *path) { /* * strip off trailing components unless we can stat the directory, or we * have run out of components */ char *i = strrchr(path, '/'); if(path[0] == '\0' || dir_exists(path)) return 0; if (i != NULL) { *i = '\0'; rmkdir(path); *i = '/'; mkdir(path, 0777); } #ifdef WIN32 return 0; #else if (dir_exists(path)) return 0; else return -1; #endif } /* * callback function for enumerate API */ int _extract_callback(struct chmFile *h, struct chmUnitInfo *ui, void *context) { LONGUINT64 ui_path_len; char buffer[32768]; struct extract_context *ctx = (struct extract_context *)context; char *i; if (ui->path[0] != '/') return CHM_ENUMERATOR_CONTINUE; /* quick hack for security hole mentioned by Sven Tantau */ if (strstr(ui->path, "/../") != NULL) { /* fprintf(stderr, "Not extracting %s (dangerous path)\n", ui->path); */ return CHM_ENUMERATOR_CONTINUE; } if (snprintf(buffer, sizeof(buffer), "%s%s", ctx->base_path, ui->path) > 1024) return CHM_ENUMERATOR_FAILURE; /* Get the length of the path */ ui_path_len = strlen(ui->path)-1; /* Distinguish between files and dirs */ if (ui->path[ui_path_len] != '/' ) { FILE *fout; LONGINT64 len, remain=ui->length; LONGUINT64 offset = 0; printf("--> %s\n", ui->path); if ((fout = fopen(buffer, "wb")) == NULL) { /* make sure that it isn't just a missing directory before we abort */ char newbuf[32768]; strcpy(newbuf, buffer); i = strrchr(newbuf, '/'); *i = '\0'; rmkdir(newbuf); if ((fout = fopen(buffer, "wb")) == NULL) return CHM_ENUMERATOR_FAILURE; } while (remain != 0) { len = chm_retrieve_object(h, ui, (unsigned char *)buffer, offset, 32768); if (len > 0) { fwrite(buffer, 1, (size_t)len, fout); offset += len; remain -= len; } else { fprintf(stderr, "incomplete file: %s\n", ui->path); break; } } fclose(fout); } else { if (rmkdir(buffer) == -1) return CHM_ENUMERATOR_FAILURE; } return CHM_ENUMERATOR_CONTINUE; } int main(int c, char **v) { struct chmFile *h; struct extract_context ec; if (c < 3) { fprintf(stderr, "usage: %s <chmfile> <outdir>\n", v[0]); exit(1); } h = chm_open(v[1]); if (h == NULL) { fprintf(stderr, "failed to open %s\n", v[1]); exit(1); } printf("%s:\n", v[1]); ec.base_path = v[2]; if (! chm_enumerate(h, CHM_ENUMERATE_ALL, _extract_callback, (void *)&ec)) printf(" *** ERROR ***\n"); chm_close(h); return 0; } 

. , .


extract(). , .


 // chmlib/examples/extract.rs fn extract( root_dir: &Path, file: &mut ChmFile, item: &UnitInfo, ) -> Result<(), Box<dyn Error>> { if !item.is_file() || !item.is_normal() { //      return Ok(()); } let path = match item.path() { Some(p) => p, //     ,   None => return Ok(()), }; let mut dest = root_dir.to_path_buf(); // :  CHM       (  "/"), //     root_dir     "/". dest.extend(path.components().skip(1)); //     if let Some(parent) = dest.parent() { fs::create_dir_all(parent)?; } let mut f = File::create(dest)?; let mut start_offset = 0; // CHMLib      &[u8]    (, //      ),       //      let mut buffer = vec![0; 1 << 16]; loop { let bytes_read = file.read(item, start_offset, &mut buffer)?; if bytes_read == 0 { //     break; } else { //      start_offset += bytes_read as u64; f.write_all(&buffer)?; } } Ok(()) } 

main() , extract(), .


 // chmlib/examples/extract.rs fn main() { let args: Vec<_> = env::args().skip(1).collect(); if args.len() != 2 || args.iter().any(|arg| arg.contains("-h")) { println!("Usage: extract <chm-file> <out-dir>"); return; } let mut file = ChmFile::open(&args[0]).expect("Unable to open the file"); let out_dir = PathBuf::from(&args[1]); file.for_each(Filter::all(), |file, item| { match extract(&out_dir, file, &item) { Ok(_) => Continuation::Continue, Err(e) => { eprintln!("Error: {}", e); Continuation::Stop }, } }); } 

CHM- HTML-, -.


 $ cargo run --example extract -- ./topics.classic.chm ./extracted $ tree ./extracted ./extracted ├── default.html ├── BrowserForward.html ... ├── Images │ ├── Commands │ │ └── RealWorld │ │ ├── BrowserBack.bmp ... ├── script │ ├── _community │ │ └── disqus.js │ ├── hs-common.js ... └── userinterface.html $ firefox topics.classic/default.html ( default.html  Firefox) 

JavaScript ( - Microsoft Help), , .


Et ensuite?


chmlib , , crates.io.


:


  • ChmFile::for_each() ChmFile::for_each_item_in_dir() , , .
  • , ChmFile Continuation::Continue . , F: FnMut(&mut ChmFile, UnitInfo) -> C C: Into<Continuation> , impl From<()> for Continuation .
  • (, extract()) ChmFile::for_each() . impl<E> From<Result<(), E>> for Continuation where E: Error + 'static .
  • - std::fs::File . , ChmFile::read() - std::io::Writer .

Source: https://habr.com/ru/post/fr474666/


All Articles