@file-viewer/renderer-chm 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ //! Stable, non-panicking error surface for untrusted CHM input.
2
+
3
+ use thiserror::Error;
4
+
5
+ #[derive(Debug, Error)]
6
+ pub enum ParseError {
7
+ #[error("not a valid CHM file: {0}")]
8
+ Header(&'static str),
9
+ #[error("malformed CHM directory: {0}")]
10
+ Directory(&'static str),
11
+ #[error("malformed MSCompressed metadata: {0}")]
12
+ Compression(&'static str),
13
+ #[error("LZX stream is malformed: {0}")]
14
+ Lzx(&'static str),
15
+ #[error("entry not found: {0}")]
16
+ NotFound(String),
17
+ #[error("entry uses unsupported storage section {0}")]
18
+ UnsupportedSection(u32),
19
+ #[error("{0}")]
20
+ ResourceLimit(String),
21
+ #[error("integer or range calculation overflow")]
22
+ Overflow,
23
+ }
24
+
25
+ pub type ParseResult<T> = Result<T, ParseError>;
26
+
27
+ /// Public, stable error surface consumed by the Worker integration.
28
+ #[derive(Debug, Error)]
29
+ pub enum CoreError {
30
+ #[error("{0}")]
31
+ Format(ParseError),
32
+ #[error("{0}")]
33
+ Limit(String),
34
+ #[error("{0}")]
35
+ UnsafePath(String),
36
+ #[error("archive is disposed")]
37
+ Disposed,
38
+ }
39
+
40
+ impl CoreError {
41
+ #[must_use]
42
+ pub fn code(&self) -> &'static str {
43
+ match self {
44
+ Self::Limit(_) => "CHM_LIMIT_EXCEEDED",
45
+ Self::UnsafePath(_) => "CHM_UNSAFE_PATH",
46
+ Self::Disposed => "CHM_DISPOSED",
47
+ Self::Format(ParseError::NotFound(_)) => "CHM_NOT_FOUND",
48
+ Self::Format(ParseError::Lzx(_)) => "CHM_LZX_ERROR",
49
+ Self::Format(ParseError::Header(_)) => "CHM_BAD_HEADER",
50
+ Self::Format(_) => "CHM_CORRUPT",
51
+ }
52
+ }
53
+ }
54
+
55
+ impl From<ParseError> for CoreError {
56
+ fn from(error: ParseError) -> Self {
57
+ match error {
58
+ ParseError::ResourceLimit(message) => Self::Limit(message),
59
+ error => Self::Format(error),
60
+ }
61
+ }
62
+ }
63
+
64
+ pub type CoreResult<T> = Result<T, CoreError>;
@@ -0,0 +1,104 @@
1
+ //! Browser-native CHM parser and random-access LZX reader.
2
+ //!
3
+ //! The container/LZX implementation is adapted from the MIT clean-room RustChm and
4
+ //! FastChm projects. The public wrapper adds bounded allocations, HTML Help metadata,
5
+ //! sitemap parsing, binary TOC/index discovery, and a `wasm-bindgen` API.
6
+
7
+ mod chm;
8
+ mod core;
9
+ mod error;
10
+ mod lzx;
11
+ mod metadata;
12
+ mod sitemap;
13
+
14
+ pub use core::{ArchiveCore, ArchiveEntry, Limits, Manifest};
15
+ pub use error::{CoreError, CoreResult};
16
+
17
+ #[cfg(target_arch = "wasm32")]
18
+ mod wasm {
19
+ use serde::Serialize;
20
+ use wasm_bindgen::prelude::*;
21
+
22
+ use crate::{ArchiveCore, CoreError, Limits};
23
+
24
+ fn js_error(error: CoreError) -> JsValue {
25
+ js_sys_error(&format!("{}: {}", error.code(), error))
26
+ }
27
+
28
+ fn js_sys_error(message: &str) -> JsValue {
29
+ js_sys::Error::new(message).into()
30
+ }
31
+
32
+ fn serialize<T: Serialize + ?Sized>(value: &T) -> Result<JsValue, JsValue> {
33
+ serde_wasm_bindgen::to_value(value)
34
+ .map_err(|error| js_sys_error(&format!("CHM_SERIALIZE: {error}")))
35
+ }
36
+
37
+ /// A parsed CHM archive whose bytes remain private to the WASM instance.
38
+ #[wasm_bindgen]
39
+ pub struct ChmArchive {
40
+ inner: Option<ArchiveCore>,
41
+ }
42
+
43
+ #[wasm_bindgen]
44
+ impl ChmArchive {
45
+ /// Validate and open an archive. `limits` is an optional JS object using
46
+ /// camelCase fields from [`Limits`].
47
+ #[wasm_bindgen(constructor)]
48
+ pub fn new(bytes: Vec<u8>, limits: Option<JsValue>) -> Result<ChmArchive, JsValue> {
49
+ let limits = match limits {
50
+ Some(value) if !value.is_null() && !value.is_undefined() => {
51
+ serde_wasm_bindgen::from_value(value)
52
+ .map_err(|error| js_sys_error(&format!("CHM_BAD_LIMITS: {error}")))?
53
+ }
54
+ _ => Limits::default(),
55
+ };
56
+ let inner = ArchiveCore::open(bytes, limits).map_err(js_error)?;
57
+ Ok(Self { inner: Some(inner) })
58
+ }
59
+
60
+ /// Return renderer-ready metadata, topics, contents and keyword index.
61
+ pub fn manifest(&mut self) -> Result<JsValue, JsValue> {
62
+ let inner = self
63
+ .inner
64
+ .as_mut()
65
+ .ok_or_else(|| js_sys_error("CHM_DISPOSED: archive is disposed"))?;
66
+ serialize(inner.manifest().map_err(js_error)?)
67
+ }
68
+
69
+ /// Return the complete bounded directory listing.
70
+ pub fn entries(&self) -> Result<JsValue, JsValue> {
71
+ let inner = self
72
+ .inner
73
+ .as_ref()
74
+ .ok_or_else(|| js_sys_error("CHM_DISPOSED: archive is disposed"))?;
75
+ serialize(inner.entries())
76
+ }
77
+
78
+ /// Read one internal path. Compressed entries are decoded by reset block and
79
+ /// cached, so sequential topic/assets do not inflate the entire CHM.
80
+ pub fn read(&mut self, path: &str) -> Result<Box<[u8]>, JsValue> {
81
+ let inner = self
82
+ .inner
83
+ .as_mut()
84
+ .ok_or_else(|| js_sys_error("CHM_DISPOSED: archive is disposed"))?;
85
+ inner
86
+ .read(path)
87
+ .map(Vec::into_boxed_slice)
88
+ .map_err(js_error)
89
+ }
90
+
91
+ /// Release archive bytes and all decompression caches immediately.
92
+ pub fn dispose(&mut self) {
93
+ self.inner = None;
94
+ }
95
+
96
+ #[wasm_bindgen(getter, js_name = disposed)]
97
+ pub fn is_disposed(&self) -> bool {
98
+ self.inner.is_none()
99
+ }
100
+ }
101
+ }
102
+
103
+ #[cfg(target_arch = "wasm32")]
104
+ pub use wasm::ChmArchive;