Wie man ein Projekt in Rust nicht umschreibt

Sobald Sie die Schmerzschwelle des Borrow-Checker überschreiten und feststellen, dass Rust es Ihnen ermöglicht, Dinge zu tun, die in anderen Sprachen unvorstellbar (und manchmal gefährlich) sind, haben Sie möglicherweise auch den gleichen unwiderstehlichen Wunsch, alles in Rust umzuschreiben . Obwohl dies im besten Fall banal unproduktiv ist (bedeutungsloser Aufwand für mehrere Projekte), führt dies im schlimmsten Fall zu einer Verschlechterung der Codequalität (warum sind Sie im Umgang mit der Bibliothek erfahrener als der ursprüngliche Autor?)


Es wäre viel nützlicher, eine sichere Schnittstelle für die ursprüngliche Bibliothek bereitzustellen, indem der Code wiederverwendet wird.


Erste Schritte
Wir sammeln chmlib-sys
Schreiben eines sicheren Wrappers in Rust
Suchen Sie nach Elementen nach Namen
Elemente nach Filter umgehen
Lesen von Dateiinhalten
Fügen Sie Beispiele hinzu
Inhaltsverzeichnis der CHM-Datei
Entpacken Sie die CHM-Datei auf die Festplatte
Wie geht es weiter?


Dieser Artikel beschreibt ein reales Projekt. Ich musste Informationen aus vorhandenen CHM-Dateien extrahieren, aber es war keine Zeit, das Format zu verstehen. Faulheit ist der Motor des Fortschritts.

Die chmlib-Kiste wird auf crates.io veröffentlicht und ihr Quellcode ist auf GitHub verfügbar. Wenn Sie es nützlich finden oder Probleme darin finden, lassen Sie es mich über den Bugtracker wissen.

Erste Schritte


Zunächst lohnt es sich zu verstehen, wie die Arbeit mit der Bibliothek ursprünglich konzipiert wurde.


Auf diese Weise lernen Sie nicht nur, wie man es benutzt, sondern stellen auch sicher, dass alles läuft. Wenn Sie Glück haben, finden Sie sogar vorgefertigte Tests und Beispiele.

Überspringen Sie diesen Schritt nicht!

Wir werden mit CHMLib arbeiten , einer C-Bibliothek zum Lesen von Microsoft Compiled HTML Help ( .chm ) -Dateien.


Beginnen wir damit, ein neues Projekt zu erstellen und CHMLib als Git-Submodul zu verbinden:


 $ 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. 

Schauen Sie sich danach mit tree an, was sich darin befindet:


 $ 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 

Es sieht so aus, als würde die Bibliothek GNU Autotools zum Erstellen verwenden. Dies ist nicht gut, da alle Benutzer der chmlib-Kiste (und ihre Benutzer) Autotools installieren müssen.


Wir werden versuchen, diese "ansteckende" Abhängigkeit durch manuelles Sammeln des C-Codes zu beseitigen, aber dazu später mehr.

Die Dateien lzx.h und lzx.c enthalten eine Implementierung des LZX- Komprimierungsalgorithmus. Im Allgemeinen wäre es besser, eine Art Liblzx-Bibliothek zu verwenden, um kostenlose Updates und all das zu erhalten, aber vielleicht wäre es einfacher, diese Dateien dumm zu kompilieren.


enum_chmLib.c, enumdir_chmLib.c, extract_chmLib.c scheinen Beispiele für die Verwendung der Funktionen chm_enumerate (), chm_enumerate_dir (), chm_retrieve_object () zu sein. Es wird sich als nützlich erweisen ...


Die Datei test_chmLib.c enthält ein weiteres Beispiel, diesmal wird eine Seite aus der CHM-Datei auf die Festplatte extrahiert.


chm_http.c implementiert einen einfachen HTTP-Server, der eine CHM-Datei in einem Browser anzeigt. Dies wird wahrscheinlich nicht länger nützlich sein.


Also haben wir alles aussortiert, was in vendor / CHMLib / src enthalten ist. Werden wir die Bibliothek abholen?


Ehrlich gesagt ist es klein genug, um die wissenschaftliche Poke-Methode anzuwenden.


 $ 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) 

Okay, vielleicht wird dieser LZX noch benötigt ...


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

Äh ... alles?


Um sicherzustellen, dass der Code funktioniert, habe ich ein Beispiel aus dem Internet heruntergeladen:


 $ 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 

Mal sehen, wie enum_chmLib damit umgeht:


 $ ./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 

Herr, auch hier jQuery ¯ \ _ (ツ) _ / ¯


Erstellen Sie chmlib-sys


Jetzt wissen wir genug, um CHMLib in der chmlib -sys-Kiste zu verwenden , die für die Erstellung der nativen Bibliothek, die Verknüpfung mit dem Rast-Compiler und eine Schnittstelle zu C-Funktionen verantwortlich ist.


Um die Bibliothek zu erstellen, müssen Sie die Datei build.rs schreiben. Mit der cc- Kiste ruft er den C-Compiler auf und schließt weitere Freundschaften, damit alles so funktioniert, wie es sollte.


Wir haben das Glück, dass wir den größten Teil der Arbeit auf CC verlagern können, aber manchmal ist es viel schwieriger. Weitere Informationen finden Sie in der Dokumentation zu Assemblerskripten .

Fügen Sie zuerst cc als Abhängigkeit für chmlib-sys hinzu:


 $ 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 

Dann schreiben wir 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"); } 

Sie müssen Cargo auch mitteilen, dass chmlib-sys Links zur chmlib-Bibliothek enthält. Dann kann Cargo garantieren, dass es im gesamten Abhängigkeitsdiagramm nur ein Rack gibt, abhängig von der spezifischen nativen Bibliothek. Dies vermeidet undurchsichtige Fehlermeldungen über wiederholte Zeichen oder die versehentliche Verwendung inkompatibler Bibliotheken.


 --- 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" } 

Als nächstes müssen wir alle von der chmlib-Bibliothek exportierten Funktionen deklarieren, damit sie aus Rast verwendet werden können.


Dafür gibt es das wunderbare Projekt bindgen . Die C-Header-Datei wird an die Eingabe übergeben, und die Datei mit den FFI-Bindungen für Rast wird ausgegeben.


 $ 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; } 

Ich empfehle dringend, die Bedienungsanleitung von Bindgen zu lesen, wenn Sie etwas im Auspuff reparieren müssen.

In diesem Stadium ist es nützlich, einen Rauchtest zu schreiben, der überprüft, ob alles wie erwartet funktioniert und ob wir tatsächlich die Funktionen der ursprünglichen C-Bibliothek aufrufen können.


 // 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 sagt, dass alles in Ordnung zu sein scheint:


 $ 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 

Schreiben eines sicheren Wrappers in Rust


Technisch und technisch können wir jetzt CHMLib von Rasta aus aufrufen, dies erfordert jedoch einen unsicheren Heap. Es mag für ein abgedroschenes Handwerk funktionieren, aber für das Veröffentlichen auf crates.io lohnt es sich, einen sicheren Wrapper für allen unsicheren Code zu schreiben.


Wenn Sie sich die chmlib-sys-API mit dem Frachtdokument --open cargo doc --open , sehen Sie viele Funktionen, die *mut ChmFile als erstes Argument verwenden. Dies ähnelt Objekten und Methoden.


CHMLib-Header-Datei
 /* $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 */ 

Beginnen wir mit dem Datentyp, der im Konstruktor chm_open () und im Destruktor chm_close () aufruft.


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

Um die Fehlerbehandlung zu vereinfachen, verwenden wir die thiserror-Kiste , die std::error::Error automatisch implementiert.


 $ cd chmlib $ cargo add thiserror 

Nun müssen Sie herausfinden, wie std::path::Path in *const c_char . Leider ist dies aufgrund verschiedener Witze mit Kompatibilität nicht so einfach zu bewerkstelligen .


 // 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; 

Definieren Sie nun die Struktur der ChmFile . Es speichert einen Zeiger ungleich Null auf chmlib_sys :: chmFile. Wenn chm_open () einen Nullzeiger zurückgibt, bedeutet dies, dass sie die Datei aufgrund eines Fehlers nicht öffnen konnte.


 // 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, } 

Führen Sie unter Valgrind einen einfachen Test durch, um sicherzustellen, dass keine Speicherlecks auftreten. Er erstellt eine ChmFile und gibt sie sofort frei.


 // 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 sagt, es ist kein unkontrollierter Speicher mehr vorhanden:


 $ 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) 

Suchen Sie nach Artikeln nach Namen


Als nächstes folgt die Funktion 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; 

Die Suche kann fehlschlagen, daher gibt chm_resolve_object () einen Fehlercode zurück, der Erfolg oder Misserfolg meldet, und Informationen über das gefundene Objekt werden vom übergebenen Zeiger auf chmUnitInfo aufgezeichnet .


Der Typ std::mem::MaybeUninit nur für unseren Fall mit dem out-Parameter ui erstellt .


Lassen wir zunächst die UnitInfo-Struktur leer - dies ist das Rust-Äquivalent der chmUnitInfo C-Struktur. Wir werden die Felder hinzufügen, wenn wir mit dem Lesen von ChmFile beginnen.


 // 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 } } 

Beachten Sie, dass ChmFile :: find () &mut self akzeptiert, obwohl der Code auf dem Rast keine explizite Statusänderung enthält. Tatsache ist, dass die C-Implementierung alle Arten von fseek () verwendet, um sich in der Datei zu bewegen, sodass sich der interne Status während der Suche immer noch ändert.

Lassen Sie uns ChmFile :: find () in der zuvor heruntergeladenen experimentellen Datei überprüfen:


 // 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()); } 

Bypass-Elemente filtern


CHMLib bietet eine API zum Anzeigen des Inhalts einer CHM-Datei über einen Bitmaskenfilter.


Nehmen Sie die praktische Bitflags-Kiste zum Arbeiten mit Masken und Flaggen:


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

Und definieren Sie die Filter- Kontrollkästchen basierend auf den Konstanten aus 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; } } 

Wir benötigen auch einen extern "C" -Adapter für Rastovyh-Verschlüsse, der in Form eines Zeigers auf eine Funktion an C übergeben werden kann:


 // 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 enthält einen kniffligen unsicheren Code, den Sie verwenden müssen:

  • Der state muss auf die Instanz von Closure F zeigen.
  • Durch einen Abschluss ausgeführter Rasta-Code kann Panik auslösen. Es sollte die Grenze zwischen Rast und C nicht überschreiten, da Stack-Promotion in verschiedenen Sprachen undefiniertes Verhalten ist. Eine mögliche Panik sollte mit std::panic::catch_unwind() abgefangen werden.
  • Ein Zeiger auf chmlib_sys :: chmFile, der an function_wrapper übergeben wird, wird auch in der aufrufenden ChmFile gespeichert. Während der Dauer des Aufrufs müssen Sie sicherstellen, dass nur der Abschluss chmlib_sys :: chmFile manipulieren kann. Andernfalls kann eine Race-Bedingung auftreten.
  • Der Abschluss muss &mut ChmFile , und dafür müssen Sie ein temporäres Objekt auf dem Stapel unter Verwendung des vorhandenen Zeigers erstellen. Wenn in diesem Fall der ChmFile-Destruktor ausgeführt wird, wird chmlib_sys :: chmFile zu früh freigegeben. Um dieses Problem zu lösen, gibt es std::mem::ManuallyDrop .

So wird function_wrapper verwendet, um ChmFile::for_each() zu implementieren:


 // 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, ); } } } 

Beachten Sie, wie der Parameter F mit der generischen Funktion function_wrapper interagiert. Diese Technik wird häufig verwendet, wenn Sie den Rust-Verschluss über FFI an Code in einer anderen Sprache übergeben müssen.

Dateiinhalt lesen


Die letzte Funktion, die wir benötigen, ist für das Lesen der Datei mit chm_retrieve_object () verantwortlich.


Die Implementierung ist ziemlich trivial. Dies ähnelt einem typischen std :: io :: Read-Merkmal mit Ausnahme eines expliziten Datei-Offsets.


 // 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; 

Natürlich wäre es schön, eine detailliertere Fehlermeldung zu haben als "Fehler beim Lesen", aber nach dem Quellcode zu urteilen, unterscheidet chm_retrieve_object () nicht besonders zwischen Fehlern:


  • gibt 0 zurück, wenn die Datei bis zum Ende gelesen wird;
  • Gibt 0 für ungültige Argumente zurück: Nullzeiger oder Grenzüberschreitungen;
  • Gibt −1 zurück, wenn Fehler beim Lesen von Dateien vom System auftreten (und füllt errno aus).
  • Gibt −1 für Dekomprimierungsfehler zurück, ohne die Datenbeschädigung zu unterscheiden und beispielsweise die Unfähigkeit, über malloc () Speicher für einen temporären Puffer zuzuweisen.

Sie können ChmFile :: read () mit Dateien mit bekanntem Inhalt testen:


 // 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" )); } 

Beispiele hinzufügen


Wir haben die meisten APIs der CHMLib-Bibliothek behandelt, und viele wären damit fertig geworden, wenn die Portierung erfolgreich abgeschlossen worden wäre. Es wäre jedoch schön, unser Rack noch benutzerfreundlicher zu gestalten. — , 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), , .


Was weiter?


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/de474666/


All Articles