From 14b56f19dfcd143fe21a1f3f0cb6141b88b9f81a Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:03:38 -0700 Subject: [PATCH] Chore: OneNote converter: Refactor to allow debugging the import process, reduce use of "unsafe" (#13300) --- packages/onenote-converter/.gitignore | 1 + packages/onenote-converter/Cargo.toml | 2 +- packages/onenote-converter/README.md | 19 ++- .../single-page/Open Notebook.onetoc2 | Bin 0 -> 4158 bytes .../single-page/Untitled Section.one | Bin 0 -> 13714 bytes packages/onenote-converter/node_functions.js | 12 -- packages/onenote-converter/src/lib.rs | 37 ++--- packages/onenote-converter/src/notebook.rs | 18 +-- .../src/page/embedded_file.rs | 9 +- packages/onenote-converter/src/page/image.rs | 9 +- .../one/property_set/page_series_node.rs | 2 +- .../src/parser/onenote/mod.rs | 53 ++----- .../src/parser/onestore/mod.rs | 2 +- .../onenote-converter/src/parser/utils.rs | 31 ---- packages/onenote-converter/src/section.rs | 44 ++---- .../src/utils/file_api/api.rs | 45 ++++++ .../src/utils/file_api/mod.rs | 23 +++ .../src/utils/file_api/native_driver.rs | 64 ++++++++ .../src/utils/file_api/wasm_driver.rs | 140 ++++++++++++++++++ .../src/{utils.rs => utils/mod.rs} | 88 ++++------- packages/onenote-converter/tests/convert.rs | 37 +++++ 21 files changed, 413 insertions(+), 223 deletions(-) create mode 100644 packages/onenote-converter/assets/test-data/single-page/Open Notebook.onetoc2 create mode 100644 packages/onenote-converter/assets/test-data/single-page/Untitled Section.one create mode 100644 packages/onenote-converter/src/utils/file_api/api.rs create mode 100644 packages/onenote-converter/src/utils/file_api/mod.rs create mode 100644 packages/onenote-converter/src/utils/file_api/native_driver.rs create mode 100644 packages/onenote-converter/src/utils/file_api/wasm_driver.rs rename packages/onenote-converter/src/{utils.rs => utils/mod.rs} (57%) create mode 100644 packages/onenote-converter/tests/convert.rs diff --git a/packages/onenote-converter/.gitignore b/packages/onenote-converter/.gitignore index e32c5c7723..21d9049d54 100644 --- a/packages/onenote-converter/.gitignore +++ b/packages/onenote-converter/.gitignore @@ -1,5 +1,6 @@ /target /output +/test-output /.idea *.iml diff --git a/packages/onenote-converter/Cargo.toml b/packages/onenote-converter/Cargo.toml index 1996fdced3..23a8a1d613 100644 --- a/packages/onenote-converter/Cargo.toml +++ b/packages/onenote-converter/Cargo.toml @@ -39,4 +39,4 @@ features = [ ] [lib] -crate-type = ["cdylib"] \ No newline at end of file +crate-type = ["cdylib", "lib"] \ No newline at end of file diff --git a/packages/onenote-converter/README.md b/packages/onenote-converter/README.md index 2ed57c6175..7fa28a02ba 100644 --- a/packages/onenote-converter/README.md +++ b/packages/onenote-converter/README.md @@ -43,6 +43,8 @@ After this, the HTML should look the same and is ready to be imported by the Imp - package.json -> where the project is built - node_functions.js -> where the custom-made functions used inside rust goes ... + - tests -> Integration tests + ... - pkg -> artifact folder generated in the build step - onenote_converter.js -> main file ... @@ -58,7 +60,7 @@ To work with the project you will need: ### Running tests -Tests for the project are located in the `lib` packages, but to make it work it is necessary to build this project first: +Most tests for the project are located in the `lib` packages, but to make it work it is necessary to build this project first: `IS_CONTINUOUS_INTEGRATION=1 yarn build # for production build` or @@ -71,6 +73,21 @@ cd ../lib IS_CONTINUOUS_INTEGRATION=1 yarn test services/interop/InteropService_Importer_OneNote.test. ``` +Other tests are written in Rust. To run these tests, use `cargo test`: +``` +cd packages/onenote-converter +cargo test +``` + +### Debugging tests + +Suppose that the importer's Rust code is failing to parse a specific `example.one` file. In this case, it may be useful to step through part of the import process in a debugger. If using VSCode, this can be done by: +1. Adding a new test to `tests/convert.rs` that runs `convert()` on the `example.one` file. +2. Setting up Rust and Rust debugging. See [the relevant VSCode documentation](https://code.visualstudio.com/docs/languages/rust#_debugging) for details. +3. Clicking the "Debug" button for the test added in step 1. This button should be provided by extensions set up in step 2. + + + ### Developing When working with the Rust code you will probably rather run `yarn buildDev` since it is faster and it has more logging messages (they can be disabled in the macro `log!()`) diff --git a/packages/onenote-converter/assets/test-data/single-page/Open Notebook.onetoc2 b/packages/onenote-converter/assets/test-data/single-page/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..e541aea5fc47c8b90abb1b2db6424c6427d87f86 GIT binary patch literal 4158 zcmZ3e|KIuM`!b(-j|5(YE0x7FK4=KNf6v)%0z={Jmksy!GN1wbyR!tOCw}!@dO>{R z+Y{?w|9%PJ5-Kh9z2n^*AIP&HD*Ns(SOSqt(;F!QL zApx4tY=B~`PMdj4^7nWz&5_ku?Y_knl$Tf%8XB;PBj-u)UB*p^MGtku%;$!h&qIP< zkeir*7^D|uF9?^JGlwDD^CT#yy647n-*p>o10MqeCnM0y8=4pxfI-q2BR?4~UuY=r*?v0QZ*IozPz=|CLKg%euFZ-%H#K>PzMmxAr}j(neG+-{04T(u1;n8Yc?=~CnLwPwkjjw4pui9eq?5s7`9L{6 z1`wGFREY?|7-k-HcQb$zZv!ZTm>9mvFsSSUS`FfDpR<|)6kHfd03*1VfvQ1(xnKd$ z&R_Rh7O&#shC@e6%wKQU~Fc_^cf zCw0}UOb+>t?mLWY?DecEf7%|5*=9F_IcA zTp&3UJyYSyYrs+mJ>lcZ#|P0$9TM{lBC{Q1U;vuM(7*!E(xAMBD_et-{FZt5NX**6 z92$p_BET5{J(6K5f;~bBBUNKF1e~zZ4Pl2SE6xil7=|zexbXZCH}?{edG0w?t?{PQ zJ#j~jqW**uMrDD`a&V?Vx10mXa`bG4)pGRo0kj;w?8jy~xI{v?99Ttx0Vw#;vm`b{ rz_}9L5Lk19^MVV8As8hE(6#9Kfi?6stYm~bXOtQZfzc2cWFY_mr8kZ1 literal 0 HcmV?d00001 diff --git a/packages/onenote-converter/assets/test-data/single-page/Untitled Section.one b/packages/onenote-converter/assets/test-data/single-page/Untitled Section.one new file mode 100644 index 0000000000000000000000000000000000000000..88fcb9732f044f6ef2629f6be51e7de3f956f3df GIT binary patch literal 13714 zcmeHN3viUx6+XL{O|oPO1R_MTfhDA1apmO#sg*|{MWjGcf`dpQJQ6@22?8P&%P$SW zAWQ*4MkG#Vq+pd=iZ~7}7Se*n8kJ6|g%$%AW~z`5Ey9dg#D3@g`9pHg%>%TZ>CE1l zn@!Gt_MCIi{qA|(KWFcs^!VE2J4e6zi*b(~{pPcOtoY*mzgl-W+HGg|hd*yO_rQAd z?_Qs*n|^FZ#do*%f9>iwQh)LWVSJ#5(p8gj@3Ln)7w?&px9_tKfAbZbox00Up$P@a z!_za$MvU?0uDEMK;e^z{Ip@!B^_|kaed+w3e|RdR%av|hd@K_gRMSEv~HlF6wBASgkmkMbHMQ9v8r)ydx;NH~DGz8X`E9oB*wWye~1Ij$M~~8y+z~f70#yYh5vA`bMYwlSlJ@ zwRCp+%+!*DdVzgd-cTaAoE$~J4pSP-WO;jEsM0c6&eA|=LL_7^Pyo-*Dlr&%(ejoi zzeb0JuKEFb(bEm^OS-zka2jP5FUhJf0I25MxdmAr&VcMmc5XpdPXxd{!_F

LWg4 zbM4%MtX3t0dy$=6kkv}IWUrlDkkxti;8{DjAgfH~K56F`WHp+(tL@x^tSZ>W|;1;%Q${nf5JwBS&-3pgX}g0Q4ey0@AcC0|Ex$K)!JLfX&`lfOe!9i`qbBB=-zh|z8PXDxIk#dA;SV2(iaI0xb6$Ns zMlnt0Cz#rXWZ1BWO8ps=Oo`%5mYN)EO@cTj;vj4{zarqg*W}o55(K6NYy~pua4T$f zG8Ww$lWU+<%(j^AFw-z^!EBG&0h6I{D`rQ`1lky;q?2J<-5Mo88_X%0lzklTguub? zR#pyTSB5aPJEOd%Zv|$UHdmVjdIEdn@b{2M@TD{q8Mj~#!Ysw)qKNA@Jk%pZs8BJu zFye|rYYXYkLnSs>i2n#?IaoBM&;}U42XimxY0N8_Ut+2-h1y|e)TpekQQH<}Bdg|x zjm>2fiPjHc#q6jSmoHz&$6%%cX80U3xE4*Vc=;C-PnzZk!XlG{Rw)>q0g*gWwm@Cp0nF&t_>*bHpnJlMsy)!LB4HoO-QI65m-uH-G?O%G!x68q;`nA)(4EcoCd zM1^RKBJDa+cCdx#crLDq`e4>-91SGx{wERY2Xd0=FEI)wLrw_(?1`GO7iLSyn;4_; z8ce?~LcavF9;|#A!p2-w-_sC$W>bVl;pa@6h3eZy=U69Pky@&zuLUWt!@ma{xm8oDgv)NIAGR?s2dzTSmwbzHi_Xm51%!_(>qq( zj2)9@Vf`)Z0*N=jWjW2-vI}*04DkoMwF;AW%>(fg9BkgTK{Wx>7APp#!Ut=4GI}`0 z^ue~=rR#(U`1Kk!Zca8K<|^t7p0A`RojMPg-V8-|S`{54I)DdGkOF{0V>_c` zdph;Xf3_LQVpe50uutuq={_$U)Ca^3uBy4p`de4EiwIpulfZ6E4?g%{qpsuncDhc1 zwJ}B~zDgoADh%(Y_Q>D9NhN%~rXd>_L)9EwfCyZygInE9yPN10R(L~4m1Ds$2%C?9 zUJM0O@k!*asTd)?c1^c7ILT_H+7Fy z(At#m-g`;9D#mR_G|~XSq^otfK!`?C?D8euz0%FMvdarG^7HMfGI)>1zuwAiA(a9eCa=DW3XWL13Bbd|7X;gSS3+>k#gvxP9Xlrm@b(dMLLeKj_bt*qvm0Lb#^X zB^hVbX;xn(60)`Wn4qo zned?(YyYcm)#*CbJUt>^2i!Hdi%2u?58Xuwvciqg!5(2dw;(GNDBhCq=BSAZ2Li~c zIb2QDF<8h0+2eCoB)zubgVC>^`C?+%<6F9kb#N2FBTx`AU;XA3g|_vX*m2HpUe4Wj z`1#|9#v~pSFCasoj_QXRl9Gz2?X1=5pU&=PU3rj;wd$!-9N=J^| zKKW0H{VSJb2}=`QLtT%DIwn85_td}7j4VqU*QIRDx-PEA1zF)X*z=)}>%JAFFNpEp z@fqZjO}MPtjG1~at1I_nCvLy3YV^hQdp7^LW6H&}v!|KnJMD}=RW)q*dRlP)6Q}A; z_N#bpF04(eu~wCoH@$x6)W)Z}EvqgWzW?yFvO~`IPHg!{YjVt2^Bf|0SR{&V5E4h( zBqH4Du{vJ@#nkTAr?Z@h{BHo$ezIxv&b`I{JQESZ{vX*SdWUe9bGm7pQfv}D_`@di zR5oo>rA?v-(^*b|C{x9%(rpqv=*;?IsO869BLk~D=l-nCZy(yW^{+1p0b^_exEA68 zw}?7lNLXo;z?p^z79r!gx!KRzByf?z12!}*;a!^qE+TjYLX(Bj8eitWpm@s;9_=O} zN@MdvpLG~8GA;b~dpCD`^2E@p4!Kbibz}3=OrDs@b0k~-H;m0h4L8$7&#y31BGzW& zC{z8|;lf1O-i#fdZLUN9XT*{0hybWG)t0R&r*W2{O30VUC zHX$1SrVvh&Jc38Fg%F#QMb Result<()> { pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> { let mut parser = Parser::new(); - let extension: String = unsafe { get_file_extension(path) } - .unwrap() - .as_string() - .unwrap(); + let extension: String = fs_driver().get_file_extension(path); match extension.as_str() { ".one" => { - let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap(); + let _name: String = fs_driver().get_file_name(path).expect("Missing file name"); log!("Parsing .one file: {}", _name); if path.contains("OneNote_RecycleBin") { @@ -53,29 +48,24 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> { let section = parser.parse_section(path.to_owned())?; - let section_output_dir = unsafe { get_output_path(base_path, output_dir, path) } - .unwrap() - .as_string() - .unwrap(); - + let section_output_dir = fs_driver().get_output_path(base_path, output_dir, path); section::Renderer::new().render(§ion, section_output_dir.to_owned())?; } ".onetoc2" => { - let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap(); + let _name: String = fs_driver().get_file_name(path).expect("Missing file name"); log!("Parsing .onetoc2 file: {}", _name); let notebook = parser.parse_notebook(path.to_owned())?; - let notebook_name = unsafe { get_parent_dir(path) } - .expect("Input file has no parent folder") - .as_string() - .expect("Parent folder has no name"); + let notebook_name = fs_driver() + .get_parent_dir(path) + .expect("Input file has no parent folder"); + if notebook_name == "" { + panic!("Parent directory has no name"); + } log!("notebook name: {:?}", notebook_name); - let notebook_output_dir = unsafe { get_output_path(base_path, output_dir, path) } - .unwrap() - .as_string() - .unwrap(); + let notebook_output_dir = fs_driver().get_output_path(base_path, output_dir, path); log!("Notebok directory: {:?}", notebook_output_dir); notebook::Renderer::new().render(¬ebook, ¬ebook_name, ¬ebook_output_dir)?; @@ -85,3 +75,4 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> { Ok(()) } + diff --git a/packages/onenote-converter/src/notebook.rs b/packages/onenote-converter/src/notebook.rs index 460d690460..1b3251491a 100644 --- a/packages/onenote-converter/src/notebook.rs +++ b/packages/onenote-converter/src/notebook.rs @@ -2,8 +2,8 @@ use crate::parser::notebook::Notebook; use crate::parser::property::common::Color; use crate::parser::section::{Section, SectionEntry}; use crate::templates::notebook::Toc; +use crate::utils::fs_driver; use crate::utils::utils::log; -use crate::utils::{join_path, make_dir, remove_prefix}; use crate::{section, templates}; use color_eyre::eyre::Result; use palette::rgb::Rgb; @@ -20,12 +20,12 @@ impl Renderer { pub fn render(&mut self, notebook: &Notebook, name: &str, output_dir: &str) -> Result<()> { log!("Notebook name: {:?} {:?}", name, output_dir); - let _ = unsafe { make_dir(output_dir) }; + fs_driver().make_dir(output_dir)?; // let notebook_dir = unsafe { join_path(output_dir, sanitize_filename::sanitize(name).as_str()) }.unwrap().as_string().unwrap(); let notebook_dir = output_dir.to_owned(); - let _ = unsafe { make_dir(¬ebook_dir) }; + fs_driver().make_dir(¬ebook_dir)?; let mut toc = Vec::new(); @@ -41,13 +41,10 @@ impl Renderer { SectionEntry::SectionGroup(group) => { let dir_name = sanitize_filename::sanitize(group.display_name()); let section_group_dir = - unsafe { join_path(notebook_dir.as_str(), dir_name.as_str()) } - .unwrap() - .as_string() - .unwrap(); + fs_driver().join(notebook_dir.as_str(), dir_name.as_str()); log!("Section group directory: {:?}", section_group_dir); - let _ = unsafe { make_dir(section_group_dir.as_str()) }; + fs_driver().make_dir(section_group_dir.as_str())?; let mut entries = Vec::new(); @@ -84,10 +81,7 @@ impl Renderer { let section_path = renderer.render(section, notebook_dir)?; log!("section_path: {:?}", section_path); - let path_from_base_dir = unsafe { remove_prefix(section_path, base_dir.as_str()) } - .unwrap() - .as_string() - .unwrap(); + let path_from_base_dir = String::from(fs_driver().remove_prefix(§ion_path, &base_dir)); log!("path_from_base_dir: {:?}", path_from_base_dir); Ok(templates::notebook::Section { name: section.display_name().to_string(), diff --git a/packages/onenote-converter/src/page/embedded_file.rs b/packages/onenote-converter/src/page/embedded_file.rs index 1245a30e1e..1ebeede0ed 100644 --- a/packages/onenote-converter/src/page/embedded_file.rs +++ b/packages/onenote-converter/src/page/embedded_file.rs @@ -1,8 +1,8 @@ use crate::page::Renderer; use crate::parser::contents::EmbeddedFile; use crate::parser::property::embedded_file::FileType; +use crate::utils::fs_driver; use crate::utils::utils::log; -use crate::utils::{join_path, write_file}; use color_eyre::eyre::ContextCompat; use color_eyre::Result; use std::path::PathBuf; @@ -12,12 +12,9 @@ impl<'a> Renderer<'a> { let content; let filename = self.determine_filename(file.filename())?; - let path = unsafe { join_path(self.output.as_str(), filename.as_str()) } - .unwrap() - .as_string() - .unwrap(); + let path = fs_driver().join(self.output.as_str(), filename.as_str()); log!("Rendering embedded file: {:?}", path); - let _ = unsafe { write_file(path.as_str(), file.data()) }; + fs_driver().write_file(&path, file.data())?; let file_type = Self::guess_type(file); diff --git a/packages/onenote-converter/src/page/image.rs b/packages/onenote-converter/src/page/image.rs index 03cf7b4d91..9fe641428e 100644 --- a/packages/onenote-converter/src/page/image.rs +++ b/packages/onenote-converter/src/page/image.rs @@ -1,7 +1,7 @@ use crate::page::Renderer; use crate::parser::contents::Image; use crate::utils::utils::log; -use crate::utils::{join_path, px, write_file, AttributeSet, StyleSet}; +use crate::utils::{fs_driver, px, AttributeSet, StyleSet}; use color_eyre::Result; impl<'a> Renderer<'a> { @@ -10,12 +10,9 @@ impl<'a> Renderer<'a> { if let Some(data) = image.data() { let filename = self.determine_image_filename(image)?; - let path = unsafe { join_path(self.output.as_str(), filename.as_str()) } - .unwrap() - .as_string() - .unwrap(); + let path = fs_driver().join(self.output.as_str(), filename.as_str()); log!("Rendering image: {:?}", path); - let _ = unsafe { write_file(path.as_str(), data) }; + fs_driver().write_file(path.as_str(), data)?; let mut attrs = AttributeSet::new(); let mut styles = StyleSet::new(); diff --git a/packages/onenote-converter/src/parser/one/property_set/page_series_node.rs b/packages/onenote-converter/src/parser/one/property_set/page_series_node.rs index d4796e15ef..ae9db72113 100644 --- a/packages/onenote-converter/src/parser/one/property_set/page_series_node.rs +++ b/packages/onenote-converter/src/parser/one/property_set/page_series_node.rs @@ -1,4 +1,3 @@ -use crate::log_warn; use crate::parser::errors::{ErrorKind, Result}; use crate::parser::fsshttpb::data::cell_id::CellId; use crate::parser::fsshttpb::data::exguid::ExGuid; @@ -9,6 +8,7 @@ use crate::parser::one::property::{simple, PropertyType}; use crate::parser::one::property_set::PropertySetId; use crate::parser::onestore::object::Object; use crate::parser::shared::guid::Guid; +use crate::utils::utils::log_warn; /// A page series. /// diff --git a/packages/onenote-converter/src/parser/onenote/mod.rs b/packages/onenote-converter/src/parser/onenote/mod.rs index a8c9092c1f..a14618e592 100644 --- a/packages/onenote-converter/src/parser/onenote/mod.rs +++ b/packages/onenote-converter/src/parser/onenote/mod.rs @@ -4,11 +4,8 @@ use crate::parser::onenote::notebook::Notebook; use crate::parser::onenote::section::{Section, SectionEntry, SectionGroup}; use crate::parser::onestore::parse_store; use crate::parser::reader::Reader; -use crate::parser::utils::{exists, is_directory, read_dir, read_file}; +use crate::utils::fs_driver; use crate::utils::utils::log; -use crate::utils::{get_dir_name, get_file_extension, get_file_name, join_path}; -use std::panic; -use web_sys::js_sys::Uint8Array; pub(crate) mod content; pub(crate) mod embedded_file; @@ -26,7 +23,6 @@ pub(crate) mod rich_text; pub(crate) mod section; pub(crate) mod table; -extern crate console_error_panic_hook; extern crate lazy_static; /// The OneNote file parser. @@ -35,7 +31,6 @@ pub struct Parser; impl Parser { /// Create a new OneNote file parser. pub fn new() -> Parser { - panic::set_hook(Box::new(console_error_panic_hook::hook)); Parser {} } @@ -46,9 +41,7 @@ impl Parser { /// sections from the folder that the table of contents file is in. pub fn parse_notebook(&mut self, path: String) -> Result { log!("Parsing notebook: {:?}", path); - let file_content = unsafe { read_file(path.as_str()) }.unwrap(); - let array = Uint8Array::new(&file_content); - let data = array.to_vec(); + let data = fs_driver().read_file(&path)?; let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?; let store = parse_store(&packaging)?; @@ -56,29 +49,20 @@ impl Parser { return Err(ErrorKind::NotATocFile { file: path }.into()); } - let base_dir = unsafe { get_dir_name(path.as_str()) } - .expect("base dir not found") - .as_string() - .unwrap(); + let base_dir = fs_driver().get_dir_name(&path); let sections = notebook::parse_toc(store.data_root())? .iter() - .map(|name| { - let result = unsafe { join_path(base_dir.as_str(), name) } - .unwrap() - .as_string() - .unwrap(); - return result; - }) + .map(|name| fs_driver().join(&base_dir, name)) .filter(|p| !p.contains("OneNote_RecycleBin")) .filter(|p| { - let is_file = match unsafe { exists(p.as_str()) } { + let is_file = match fs_driver().exists(p) { Ok(is_file) => is_file, Err(_err) => false, }; return is_file; }) .map(|p| { - let is_dir = unsafe { is_directory(p.as_str()) }.unwrap(); + let is_dir = fs_driver().is_directory(&p)?; if !is_dir { self.parse_section(p).map(SectionEntry::Section) } else { @@ -96,9 +80,7 @@ impl Parser { /// OneNote section. pub fn parse_section(&mut self, path: String) -> Result

{ log!("Parsing section: {:?}", path); - let file_content = unsafe { read_file(path.as_str()) }.unwrap(); - let array = Uint8Array::new(&file_content); - let data = array.to_vec(); + let data = fs_driver().read_file(path.as_str())?; let packaging = OneStorePackaging::parse(&mut Reader::new(&data))?; let store = parse_store(&packaging)?; @@ -106,25 +88,20 @@ impl Parser { return Err(ErrorKind::NotASectionFile { file: path }.into()); } - let filename = unsafe { get_file_name(path.as_str()) } - .expect("file without file name") - .as_string() - .unwrap(); + let filename = fs_driver() + .get_file_name(&path) + .expect("file without file name"); section::parse_section(store, filename) } fn parse_section_group(&mut self, path: String) -> Result { - let display_name = unsafe { get_file_name(path.as_str()) } - .expect("file without file name") - .as_string() - .unwrap(); + let display_name = fs_driver() + .get_file_name(path.as_str()) + .expect("file without file name"); - if let Some(entries) = unsafe { read_dir(path.as_str()) } { + if let Ok(entries) = fs_driver().read_dir(&path) { for entry in entries { - let ext = unsafe { get_file_extension(entry.as_str()) } - .unwrap() - .as_string() - .unwrap(); + let ext = fs_driver().get_file_extension(&entry); if ext == ".onetoc2" { return self.parse_notebook(entry).map(|group| SectionGroup { display_name, diff --git a/packages/onenote-converter/src/parser/onestore/mod.rs b/packages/onenote-converter/src/parser/onestore/mod.rs index 8a8e747099..e1830ba9bb 100644 --- a/packages/onenote-converter/src/parser/onestore/mod.rs +++ b/packages/onenote-converter/src/parser/onestore/mod.rs @@ -31,7 +31,7 @@ impl<'a> OneStore<'a> { self.schema } - pub(crate) fn data_root(&'a self) -> &'a ObjectSpace { + pub(crate) fn data_root(&self) -> &ObjectSpace { &self.data_root } diff --git a/packages/onenote-converter/src/parser/utils.rs b/packages/onenote-converter/src/parser/utils.rs index 372de64384..498321b985 100644 --- a/packages/onenote-converter/src/parser/utils.rs +++ b/packages/onenote-converter/src/parser/utils.rs @@ -3,8 +3,6 @@ use itertools::Itertools; use std::collections::HashMap; use std::fmt; use std::fmt::Display; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsValue; use widestring::U16CString; pub(crate) struct AttributeSet(HashMap<&'static str, String>); @@ -69,35 +67,6 @@ impl Display for StyleSet { } } -#[wasm_bindgen(module = "/node_functions.js")] -extern "C" { - #[wasm_bindgen(js_name = isDirectory, catch)] - pub unsafe fn is_directory(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = readDir, catch)] - unsafe fn read_dir_js(path: &str) -> std::result::Result; -} - -pub unsafe fn read_dir(path: &str) -> Option> { - let result_ptr = read_dir_js(path).unwrap(); - - let result_str: String = match result_ptr.as_string() { - Some(x) => x, - _ => String::new(), - }; - let names: Vec = result_str.split('\n').map(|s| s.to_string()).collect_vec(); - Some(names) -} - -#[wasm_bindgen(module = "fs")] -extern "C" { - #[wasm_bindgen(js_name = readFileSync, catch)] - pub unsafe fn read_file(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = existsSync, catch)] - pub unsafe fn exists(path: &str) -> std::result::Result; -} - pub(crate) trait Utf16ToString { fn utf16_to_string(&self) -> Result; } diff --git a/packages/onenote-converter/src/section.rs b/packages/onenote-converter/src/section.rs index 8c3dc22fc5..a0a71a53b0 100644 --- a/packages/onenote-converter/src/section.rs +++ b/packages/onenote-converter/src/section.rs @@ -1,6 +1,6 @@ use crate::parser::section::Section; +use crate::utils::fs_driver; use crate::utils::utils::log; -use crate::utils::{join_path, make_dir, remove_prefix, write_file}; use crate::{page, templates}; use color_eyre::eyre::Result; use std::collections::HashSet; @@ -19,15 +19,10 @@ impl Renderer { } pub fn render(&mut self, section: &Section, output_dir: String) -> Result { - let section_dir = unsafe { - join_path( - output_dir.as_str(), - sanitize_filename::sanitize(section.display_name()).as_str(), - ) - } - .unwrap() - .as_string() - .unwrap(); + let section_dir = fs_driver().join( + output_dir.as_str(), + sanitize_filename::sanitize(section.display_name()).as_str(), + ); log!( "section_dir: {:?} \n output_dir: {:?}", section_dir, @@ -35,7 +30,7 @@ impl Renderer { ); log!("Rendering section: {:?}", section_dir); - let _ = unsafe { make_dir(section_dir.as_str()) }; + fs_driver().make_dir(section_dir.as_str())?; let mut toc = Vec::new(); let mut fallback_title_index = 0; @@ -52,38 +47,27 @@ impl Renderer { let file_name = self.determine_page_filename(&file_name)?; let file_name = sanitize_filename::sanitize(file_name + ".html"); - let page_path = unsafe { join_path(section_dir.as_str(), file_name.as_str()) } - .unwrap() - .as_string() - .unwrap(); + let page_path = fs_driver().join(section_dir.as_str(), file_name.as_str()); let mut renderer = page::Renderer::new(section_dir.clone(), self); let page_html = renderer.render_page(page)?; log!("Creating page file: {:?}", page_path); - let _ = unsafe { write_file(&page_path, page_html.as_bytes()) }; + fs_driver().write_file(&page_path, page_html.as_bytes())?; let page_path_without_basedir = - unsafe { remove_prefix(page_path, output_dir.as_str()) } - .unwrap() - .as_string() - .unwrap(); + String::from(fs_driver().remove_prefix(&page_path, output_dir.as_str())); toc.push((title, page_path_without_basedir, page.level())) } } let toc_html = templates::section::render(section.display_name(), toc)?; - let toc_file = unsafe { - join_path( - output_dir.as_str(), - format!("{}.html", section.display_name()).as_str(), - ) - } - .unwrap() - .as_string() - .unwrap(); + let toc_file = fs_driver().join( + output_dir.as_str(), + format!("{}.html", section.display_name()).as_str(), + ); log!("ToC: {:?}", toc_file); - let _ = unsafe { write_file(toc_file.as_str(), toc_html.as_bytes()) }; + fs_driver().write_file(toc_file.as_str(), toc_html.as_bytes())?; Ok(section_dir) } diff --git a/packages/onenote-converter/src/utils/file_api/api.rs b/packages/onenote-converter/src/utils/file_api/api.rs new file mode 100644 index 0000000000..affe47d17c --- /dev/null +++ b/packages/onenote-converter/src/utils/file_api/api.rs @@ -0,0 +1,45 @@ +pub type ApiResult = std::result::Result; + +pub trait FileApiDriver: Send + Sync { + fn is_directory(&self, path: &str) -> ApiResult; + fn read_dir(&self, path: &str) -> ApiResult>; + fn read_file(&self, path: &str) -> ApiResult>; + fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()>; + fn make_dir(&self, path: &str) -> ApiResult<()>; + fn exists(&self, path: &str) -> ApiResult; + + // These functions correspond to the similarly-named + // path functions. + fn get_file_name(&self, path: &str) -> Option; + fn get_file_extension(&self, path: &str) -> String; + fn get_dir_name(&self, path: &str) -> String; + fn join(&self, path_1: &str, path_2: &str) -> String; + + fn remove_prefix<'a>(&self, full_path: &'a str, prefix: &str) -> &'a str { + if full_path.starts_with(prefix) { + &full_path[prefix.len()..] + } else { + full_path + } + } + fn get_output_path(&self, input_dir: &str, output_dir: &str, file_path: &str) -> String { + let base_path = self.remove_prefix(file_path, input_dir); + let rebased_output = self.join(output_dir, base_path); + self.get_dir_name(&rebased_output) + } + + fn get_parent_dir(&self, path: &str) -> Option { + let dir_name = self.get_dir_name(path); + let result = self.get_file_name(&dir_name); + + if let Some(value) = result { + if value == "" { + None + } else { + Some(value) + } + } else { + None + } + } +} diff --git a/packages/onenote-converter/src/utils/file_api/mod.rs b/packages/onenote-converter/src/utils/file_api/mod.rs new file mode 100644 index 0000000000..1877b09699 --- /dev/null +++ b/packages/onenote-converter/src/utils/file_api/mod.rs @@ -0,0 +1,23 @@ +pub mod api; +pub use api::ApiResult; +pub use api::FileApiDriver; +use lazy_static::lazy_static; +use std::sync::Arc; + +#[cfg(target_arch = "wasm32")] +mod wasm_driver; +#[cfg(target_arch = "wasm32")] +use wasm_driver::FileApiDriverImpl; + +#[cfg(not(target_arch = "wasm32"))] +mod native_driver; +#[cfg(not(target_arch = "wasm32"))] +use native_driver::FileApiDriverImpl; + +lazy_static! { + static ref FS_DRIVER: Arc = Arc::new(FileApiDriverImpl {}); +} + +pub fn fs_driver() -> Arc { + FS_DRIVER.clone() +} diff --git a/packages/onenote-converter/src/utils/file_api/native_driver.rs b/packages/onenote-converter/src/utils/file_api/native_driver.rs new file mode 100644 index 0000000000..a86bdb9729 --- /dev/null +++ b/packages/onenote-converter/src/utils/file_api/native_driver.rs @@ -0,0 +1,64 @@ +use super::ApiResult; +use super::FileApiDriver; +use std::fs; +use std::path::Path; + +pub struct FileApiDriverImpl {} + +impl FileApiDriver for FileApiDriverImpl { + fn is_directory(&self, path: &str) -> ApiResult { + let metadata = fs::metadata(path)?; + let file_type = metadata.file_type(); + Ok(file_type.is_dir()) + } + + fn read_dir(&self, path: &str) -> ApiResult> { + let mut result: Vec = Vec::new(); + for item in fs::read_dir(path)? { + let item = item?.path(); + result.push(item.to_string_lossy().into()) + } + Ok(result) + } + + fn read_file(&self, path: &str) -> ApiResult> { + Ok(fs::read(path)?) + } + + fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> { + Ok(fs::write(path, data)?) + } + + fn exists(&self, path: &str) -> ApiResult { + Ok(fs::exists(path)?) + } + + fn make_dir(&self, path: &str) -> ApiResult<()> { + Ok(fs::create_dir(path)?) + } + + fn get_file_name(&self, path: &str) -> Option { + match Path::new(path).file_name() { + Some(name) => Some(name.to_string_lossy().into()), + None => None, + } + } + fn get_file_extension(&self, path: &str) -> String { + match Path::new(path).extension() { + Some(ext) => { + let extension = String::from(ext.to_string_lossy()); + String::from(".") + &extension + } + None => String::from(""), + } + } + fn get_dir_name(&self, path: &str) -> String { + match Path::new(path).parent() { + Some(parent) => parent.to_string_lossy().into(), + None => String::from(""), + } + } + fn join(&self, path_1: &str, path_2: &str) -> String { + Path::new(path_1).join(path_2).to_string_lossy().into() + } +} diff --git a/packages/onenote-converter/src/utils/file_api/wasm_driver.rs b/packages/onenote-converter/src/utils/file_api/wasm_driver.rs new file mode 100644 index 0000000000..da79c942a3 --- /dev/null +++ b/packages/onenote-converter/src/utils/file_api/wasm_driver.rs @@ -0,0 +1,140 @@ +use super::ApiResult; +use super::FileApiDriver; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use web_sys::js_sys; +use web_sys::js_sys::Uint8Array; + +#[wasm_bindgen(module = "/node_functions.js")] +extern "C" { + #[wasm_bindgen(js_name = mkdirSyncRecursive, catch)] + fn make_dir(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = pathSep, catch)] + fn path_sep() -> std::result::Result; + + #[wasm_bindgen(js_name = removePrefix, catch)] + fn remove_prefix(base_path: String, prefix: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = getOutputPath, catch)] + fn get_output_path( + input_dir: &str, + output_dir: &str, + file_path: &str, + ) -> std::result::Result; + + #[wasm_bindgen(js_name = normalizeAndWriteFile, catch)] + fn write_file(path: &str, data: &[u8]) -> std::result::Result; + + #[wasm_bindgen(js_name = isDirectory, catch)] + fn is_directory(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = readDir, catch)] + fn read_dir_js(path: &str) -> std::result::Result; +} + +#[wasm_bindgen(module = "fs")] +extern "C" { + #[wasm_bindgen(js_name = readFileSync, catch)] + fn read_file(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = existsSync, catch)] + fn exists(path: &str) -> std::result::Result; +} + +#[wasm_bindgen(module = "path")] +extern "C" { + #[wasm_bindgen(js_name = basename, catch)] + fn get_file_name(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = extname, catch)] + fn get_file_extension(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = dirname, catch)] + fn get_dir_name(path: &str) -> std::result::Result; + + #[wasm_bindgen(js_name = join, catch)] + fn join_path(path_1: &str, path_2: &str) -> std::result::Result; +} + +fn handle_error(error: JsValue, source: &str) -> std::io::Error { + use std::io::Error; + use std::io::ErrorKind; + + let error = js_sys::Error::from(error); + match error.name().to_string() { + _ => Error::new( + ErrorKind::Other, + String::from(format!("Err({}): {:?}", source, error)), + ), + } +} + +pub struct FileApiDriverImpl {} + +impl FileApiDriver for FileApiDriverImpl { + fn is_directory(&self, path: &str) -> ApiResult { + match is_directory(path) { + Ok(is_dir) => Ok(is_dir), + Err(e) => Err(handle_error(e, "checking is_directory")), + } + } + + fn read_dir(&self, path: &str) -> ApiResult> { + let result_ptr = read_dir_js(path).unwrap(); + + let result_str: String = match result_ptr.as_string() { + Some(x) => x, + _ => String::new(), + }; + Ok(result_str.split('\n').map(|s| s.to_string()).collect()) + } + + fn read_file(&self, path: &str) -> ApiResult> { + match read_file(path) { + Ok(file) => Ok(Uint8Array::new(&file).to_vec()), + Err(e) => Err(handle_error(e, &format!("reading file {}", path))), + } + } + + fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> { + if let Err(error) = write_file(path, data) { + Err(handle_error(error, &format!("writing file {}", path))) + } else { + Ok(()) + } + } + + fn exists(&self, path: &str) -> ApiResult { + match exists(path) { + Ok(exists) => Ok(exists), + Err(e) => Err(handle_error(e, &format!("checking exists {}", path))), + } + } + + fn make_dir(&self, path: &str) -> ApiResult<()> { + if let Err(error) = make_dir(path) { + Err(handle_error(error, &format!("mkdir {}", path))) + } else { + Ok(()) + } + } + + fn get_file_name(&self, path: &str) -> Option { + let file_name = get_file_name(path).unwrap().as_string().unwrap(); + if file_name == "" { + None + } else { + Some(file_name) + } + } + fn get_file_extension(&self, path: &str) -> String { + get_file_extension(path).unwrap().as_string().unwrap() + } + fn get_dir_name(&self, path: &str) -> String { + get_dir_name(path).unwrap().as_string().unwrap() + } + fn join(&self, path_1: &str, path_2: &str) -> String { + join_path(path_1, path_2).unwrap().as_string().unwrap() + } +} diff --git a/packages/onenote-converter/src/utils.rs b/packages/onenote-converter/src/utils/mod.rs similarity index 57% rename from packages/onenote-converter/src/utils.rs rename to packages/onenote-converter/src/utils/mod.rs index 23c797cac9..646cc99a46 100644 --- a/packages/onenote-converter/src/utils.rs +++ b/packages/onenote-converter/src/utils/mod.rs @@ -5,10 +5,12 @@ use std::collections::HashMap; use std::fmt; use std::fmt::Display; use std::sync::Mutex; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsValue; use widestring::U16CString; +mod file_api; +pub use file_api::fs_driver; +pub use file_api::FileApiDriver; + pub(crate) fn px(inches: f32) -> String { format!("{}px", (inches * 48.0).round()) } @@ -74,63 +76,8 @@ impl Display for StyleSet { } } -#[wasm_bindgen(module = "/node_functions.js")] -extern "C" { - #[wasm_bindgen(js_name = mkdirSyncRecursive, catch)] - pub unsafe fn make_dir(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = pathSep, catch)] - pub unsafe fn path_sep() -> std::result::Result; - - #[wasm_bindgen(js_name = removePrefix, catch)] - pub unsafe fn remove_prefix( - base_path: String, - prefix: &str, - ) -> std::result::Result; - - #[wasm_bindgen(js_name = getOutputPath, catch)] - pub unsafe fn get_output_path( - input_dir: &str, - output_dir: &str, - file_path: &str, - ) -> std::result::Result; - - #[wasm_bindgen(js_name = getParentDir, catch)] - pub unsafe fn get_parent_dir(input_dir: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = normalizeAndWriteFile, catch)] - pub unsafe fn write_file(path: &str, data: &[u8]) -> std::result::Result; -} - -#[wasm_bindgen(module = "fs")] -extern "C" { - // #[wasm_bindgen(js_name = writeFileSync, catch)] - // pub unsafe fn write_file(path: &str, data: &[u8]) -> std::result::Result; - - #[wasm_bindgen(js_name = readFileSync, catch)] - pub unsafe fn read_file(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = existsSync, catch)] - pub unsafe fn exists(path: &str) -> std::result::Result; -} - -#[wasm_bindgen(module = "path")] -extern "C" { - #[wasm_bindgen(js_name = basename, catch)] - pub unsafe fn get_file_name(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = extname, catch)] - pub unsafe fn get_file_extension(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = dirname, catch)] - pub unsafe fn get_dir_name(path: &str) -> std::result::Result; - - #[wasm_bindgen(js_name = join, catch)] - pub unsafe fn join_path(path_1: &str, path_2: &str) -> std::result::Result; -} - pub mod utils { - + #[cfg(target_arch = "wasm32")] macro_rules! log { ( $( $t:tt )* ) => { #[cfg(debug_assertions)] @@ -138,15 +85,32 @@ pub mod utils { } } + #[cfg(target_arch = "wasm32")] macro_rules! log_warn { ( $( $t:tt )* ) => { use crate::utils::get_current_page; - web_sys::console::warn_1(&format!("OneNoteConverter: Warning around the following page: {}", get_current_page().unwrap()).into()); + web_sys::console::warn_1(&format!("OneNoteConverter: Warning around the following page: {}", get_current_page()).into()); web_sys::console::warn_2(&format!("OneNoteConverter: ").into(), &format!( $( $t )* ).into()); } } + #[cfg(not(target_arch = "wasm32"))] + macro_rules! log { + ( $( $t:tt )* ) => { + #[cfg(debug_assertions)] + println!( $( $t )* ); + } + } + + #[cfg(not(target_arch = "wasm32"))] + macro_rules! log_warn { + ( $( $t:tt )* ) => { + use crate::utils::get_current_page; + println!("Warning: {}, near {}", &format!( $( $t )* ), get_current_page()); + } + } + pub(crate) use log; pub(crate) use log_warn; } @@ -177,7 +141,9 @@ pub fn set_current_page(page_name: String) { *current_page = Some(page_name.to_string()); } -pub fn get_current_page() -> Option { +pub fn get_current_page() -> String { let current_page = CURRENT_PAGE.lock().unwrap(); - current_page.clone() + current_page + .clone() + .unwrap_or_else(|| String::from("[None]")) } diff --git a/packages/onenote-converter/tests/convert.rs b/packages/onenote-converter/tests/convert.rs new file mode 100644 index 0000000000..1233e411f5 --- /dev/null +++ b/packages/onenote-converter/tests/convert.rs @@ -0,0 +1,37 @@ +use onenote_converter::convert; +use std::fs; +use std::path::PathBuf; + +fn get_output_dir() -> PathBuf { + PathBuf::from("./test-output") +} + +fn setup() { + let output_dir = get_output_dir(); + + if output_dir.exists() { + fs::remove_dir_all(&output_dir).unwrap(); + } + fs::create_dir(&output_dir).unwrap(); +} + +#[test] +fn convert_simple() { + setup(); + + let output_dir = get_output_dir(); + convert( + "./assets/test-data/single-page/Untitled Section.one", + &output_dir.to_string_lossy(), + "./assets/test-data/single-page/", + ) + .unwrap(); + + // Should create a table of contents file + assert!(output_dir.join("Untitled Section.html").exists()); + // Should convert the input page to an HTML file + assert!(output_dir + .join("Untitled Section") + .join("test.html") + .exists()); +}