Bagaimana tidak menulis ulang proyek di Rust

Segera setelah Anda melewati ambang rasa sakit Peminjam-Meminjam dan menyadari bahwa Rust memungkinkan Anda untuk melakukan hal-hal yang tidak dapat dibayangkan (dan kadang-kadang berbahaya) dalam bahasa lain, Anda mungkin juga memiliki keinginan yang sama tak tertahankan untuk Menulis Ulang Segalanya ke Karat . Meskipun dalam kasus terbaik hal ini tidak produktif dangkal (upaya sia-sia yang sia-sia untuk beberapa proyek), yang terburuk mengarah pada penurunan kualitas kode (setelah semua, mengapa Anda menganggap diri Anda lebih berpengalaman dalam bidang menggunakan perpustakaan daripada penulis aslinya?)


Akan jauh lebih bermanfaat untuk menyediakan antarmuka yang aman untuk perpustakaan asli dengan menggunakan kembali kodenya.


โ€ข Langkah pertama
โ€ข Kami mengumpulkan chmlib-sys
โ€ข Menulis pembungkus yang aman di Rust
โ€ข Cari item berdasarkan nama
โ€ข Lewati elemen dengan filter
โ€ข Membaca isi file
โ€ข Tambahkan contoh
โ€ข Daftar isi file CHM
โ€ข Membuka kemasan file CHM ke disk
โ€ข Apa selanjutnya?


Artikel ini membahas proyek nyata. Saya harus mengekstrak informasi dari file CHM yang ada, tetapi tidak ada waktu untuk memahami formatnya. Kemalasan adalah mesin kemajuan.

Peti chmlib diterbitkan di crates.io , dan kode sumbernya tersedia di GitHub . Jika Anda menemukan itu berguna atau menemukan masalah di dalamnya, maka beri tahu saya melalui bugtracker .

Langkah pertama


Untuk memulainya, ada baiknya memahami bagaimana pekerjaan dengan perpustakaan awalnya disusun.


Ini tidak hanya akan mengajari Anda cara menggunakannya, tetapi juga memastikan bahwa semuanya berjalan. Jika Anda beruntung, Anda bahkan akan menemukan tes dan contoh yang sudah jadi.

Jangan lewati langkah ini!

Kami akan bekerja dengan CHMLib , pustaka C untuk membaca file Microsoft Compiled HTML Help ( .chm ).


Mari kita mulai dengan membuat proyek baru dan menghubungkan CHMLib sebagai submodule 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. 

Setelah itu, lihat apa yang ada di dalam menggunakan 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 

Sepertinya perpustakaan menggunakan GNU Autotools untuk membangun. Ini tidak baik, karena semua pengguna peti chmlib (dan penggunanya) perlu menginstal Autotools.


Kami akan mencoba untuk menghilangkan ketergantungan "menular" ini dengan mengumpulkan kode C secara manual, tetapi lebih lanjut tentang itu nanti.

File lzx.h dan lzx.c berisi implementasi algoritma kompresi LZX . Secara umum, akan lebih baik menggunakan semacam perpustakaan liblzx untuk mendapatkan pembaruan secara gratis dan semua itu, tapi mungkin akan lebih mudah untuk dengan bodoh mengkompilasi file-file ini.


enum_chmLib.c, enumdir_chmLib.c, extract_chmLib.c tampaknya menjadi contoh penggunaan fungsi chm_enumerate (), chm_enumerate_dir (), chm_retrieve_object (). Ini akan berguna ...


File test_chmLib.c berisi contoh lain, kali ini mengekstraksi satu halaman dari file CHM ke disk.


chm_http.c mengimplementasikan server HTTP sederhana yang menunjukkan file .chm di browser. Ini, mungkin, tidak lagi berguna.


Jadi kami menyortir semua yang ada di vendor / CHMLib / src. Akankah kami mengumpulkan perpustakaan?


Jujur, itu cukup kecil untuk menerapkan metode poke ilmiah.


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

Oke, mungkin LZX ini masih dibutuhkan ...


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

Uh ... semua?


Untuk memastikan kode berfungsi, saya mengunduh contoh dari 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 

Mari kita lihat bagaimana enum_chmLib menanganinya:


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

Tuhan, bahkan di sini jQuery ยฏ \ _ (ใƒ„) _ / ยฏ


Bangun chmlib-sys


Sekarang kita cukup tahu untuk menggunakan CHMLib di chmlib -sys crate , yang bertanggung jawab untuk membangun perpustakaan asli, menghubungkannya dengan kompiler Rast, dan antarmuka ke fungsi C.


Untuk membangun perpustakaan Anda perlu menulis file build.rs . Dengan menggunakan cc crate, ia akan memanggil kompiler C dan melakukan pertemanan lain sehingga semuanya bekerja bersama sebagaimana mestinya.


Kami beruntung bahwa kami dapat mengalihkan sebagian besar pekerjaan ke cc, tetapi kadang-kadang jauh lebih sulit. Baca lebih lanjut di dokumentasi untuk skrip perakitan .

Pertama tambahkan cc sebagai dependensi untuk 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 

Kemudian kami menulis 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"); } 

Anda juga perlu memberi tahu Cargo bahwa tautan chmlib-sys ke perpustakaan chmlib. Kemudian Cargo dapat menjamin bahwa di seluruh grafik dependensi hanya ada satu rak, tergantung pada perpustakaan asli tertentu. Ini menghindari pesan kesalahan yang tidak jelas tentang karakter yang diulang atau penggunaan perpustakaan yang tidak kompatibel secara tidak sengaja.


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

Selanjutnya, kita perlu mendeklarasikan semua fungsi yang diekspor oleh pustaka chmlib agar dapat digunakan dari Rast.


Untuk inilah proyek bindgen yang indah itu ada . File header C diberikan ke input, dan file dengan binding FFI untuk Rast adalah output.


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

Saya sangat merekomendasikan membaca manual pengguna Bindgen jika Anda perlu memperbaiki sesuatu di knalpotnya.

Pada tahap ini, akan berguna untuk menulis tes asap yang akan memverifikasi bahwa semuanya berfungsi seperti yang diharapkan dan bahwa kita benar-benar dapat memanggil fungsi dari pustaka C asli.


 // 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 mengatakan semuanya tampaknya beres:


 $ 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 

Menulis bungkus aman di Rust


Secara teknis dan teknis, kita sekarang dapat memanggil CHMLib dari Rasta, tetapi ini membutuhkan tumpukan yang tidak aman . Ini mungkin berfungsi untuk kerajinan yang basi, tetapi untuk penerbitan di crates.io layak menulis pembungkus yang aman untuk semua kode yang tidak aman.


Jika Anda melihat chmlib-sys API menggunakan cargo doc --open , Anda dapat melihat banyak fungsi yang menggunakan *mut ChmFile sebagai argumen pertama. Ini mirip dengan objek dan metode.


File Header 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 */ 

Mari kita mulai dengan tipe data, yang memanggil chm_open () di konstruktor, dan chm_close () di destructor.


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

Untuk menyederhanakan penanganan kesalahan, kami menggunakan kotak korupsi ini , yang std::error::Error mengimplementasikan secara otomatis.


 $ cd chmlib $ cargo add thiserror 

Sekarang Anda perlu mengetahui cara mengubah std::path::Path menjadi *const c_char . Sayangnya, ini tidak mudah dilakukan karena berbagai lelucon dengan kompatibilitas .


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

Sekarang tentukan struktur ChmFile . Ini menyimpan pointer non-null ke chmlib_sys :: chmFile. Jika chm_open () mengembalikan pointer nol, maka itu berarti dia tidak bisa membuka file karena beberapa jenis kesalahan.


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

Untuk memastikan tidak ada kebocoran memori, jalankan tes sederhana di bawah Valgrind . Dia akan membuat ChmFile dan segera melepaskannya.


 // 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 mengatakan tidak ada sisa memori yang tidak terhitung:


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

Cari item berdasarkan nama


Baris berikutnya adalah fungsi 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; 

Pencarian mungkin gagal, jadi chm_resolve_object () mengembalikan kode kesalahan yang melaporkan keberhasilan atau kegagalan, dan informasi tentang objek yang ditemukan akan direkam oleh pointer yang dikirimkan ke chmUnitInfo .


Jenis std::mem::MaybeUninit dibuat hanya untuk kasus kita dengan parameter ui keluar.


Untuk saat ini, mari kita biarkan struktur UnitInfo kosong - ini adalah setara Karat dari struktur chmUnitInfo C. Kami akan menambahkan bidang ketika kami mulai membaca dari 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 } } 

Perhatikan bahwa ChmFile :: find () menerima &mut self , meskipun kode pada Rast tidak berisi perubahan status eksplisit. Faktanya adalah bahwa implementasi C menggunakan semua jenis fseek () untuk bergerak di sekitar file, sehingga keadaan internal masih berubah selama pencarian.

Mari kita periksa ChmFile :: find () pada file eksperimental yang sebelumnya kita unduh:


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

Saring item pintas


CHMLib menyediakan API untuk melihat konten file CHM melalui filter bitmask.


Ambil peti bitflag yang nyaman untuk bekerja dengan topeng dan bendera:


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

Dan tentukan kotak centang Filter berdasarkan konstanta dari 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; } } 

Kami juga membutuhkan adaptor extern "C" untuk penutupan Rastovyh, yang dapat diteruskan ke C dalam bentuk penunjuk ke fungsi:


 // 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 berisi kode tidak aman yang rumit yang harus Anda gunakan:

  • Pointer state harus menunjuk ke instance dari penutupan F.
  • Kode rasta yang dieksekusi oleh penutupan dapat menyebabkan kepanikan. Seharusnya tidak melewati batas antara Rast dan C, karena promosi tumpukan dalam berbagai bahasa adalah perilaku yang tidak ditentukan. Panic yang mungkin harus dicegat menggunakan std::panic::catch_unwind() .
  • Pointer ke chmlib_sys :: chmFile yang diteruskan ke function_wrapper juga disimpan di dalam panggilan ChmFile. Selama durasi panggilan, Anda harus memastikan bahwa hanya penutupan yang dapat memanipulasi chmlib_sys :: chmFile, jika tidak kondisi lomba dapat terjadi.
  • Penutupan harus dilewati &mut ChmFile , dan untuk ini Anda harus membuat objek sementara di stack menggunakan pointer yang ada. Namun, jika destruktor ChmFile berjalan dalam kasus ini, maka chmlib_sys :: chmFile akan dibebaskan terlalu cepat. Untuk mengatasi masalah ini, ada std::mem::ManuallyDrop .

Ini adalah bagaimana function_wrapper digunakan untuk mengimplementasikan 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, ); } } } 

Perhatikan bagaimana parameter F berinteraksi dengan fungsi generik function_wrapper. Teknik ini sering digunakan ketika Anda harus melewati penutupan Karat melalui FFI untuk kode dalam bahasa lain.

Membaca isi file


Fungsi terakhir yang kita perlukan bertanggung jawab untuk benar-benar membaca file menggunakan chm_retrieve_object ().


Implementasinya cukup sepele. Ini mirip dengan ciri khas std :: io :: Read, dengan pengecualian offset file eksplisit.


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

Tentu saja, akan lebih baik memiliki pesan kesalahan yang lebih rinci daripada "gagal dibaca", tetapi jika dilihat dari kode sumber, chm_retrieve_object () tidak terlalu membedakan antara kesalahan:


  • mengembalikan 0 ketika file dibaca sampai akhir;
  • mengembalikan 0 untuk argumen yang tidak valid: null pointer atau keluar batas;
  • mengembalikan โˆ’1 pada kesalahan dalam membaca file oleh sistem (dan mengisi errno);
  • mengembalikan โˆ’1 untuk kesalahan dekompresi, tanpa membedakan korupsi data dan, katakanlah, ketidakmampuan untuk mengalokasikan memori untuk buffer sementara melalui malloc ().

Anda dapat menguji ChmFile :: read () menggunakan file dengan konten yang diketahui:


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

Tambahkan contoh


Kami membahas sebagian besar API perpustakaan CHMLib dan banyak yang akan selesai dengan ini, mengingat porting harus diselesaikan dengan sukses. Namun, alangkah baiknya untuk membuat rak kami lebih ramah pengguna. โ€” , 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), , .


Apa selanjutnya


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


All Articles