@file-viewer/renderer-archive 2.1.7 → 2.1.8
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.
- package/README.en.md +20 -1
- package/README.md +20 -1
- package/dist/archive.js +298 -27
- package/dist/archiveFallback.d.ts +1 -0
- package/dist/archiveFallback.js +35 -0
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -30,8 +30,9 @@ const options = {
|
|
|
30
30
|
## Capabilities
|
|
31
31
|
|
|
32
32
|
- Previews ZIP, TAR, GZIP, RAR, 7z, and common archive directories.
|
|
33
|
+
- Supports encrypted archives: encrypted content is handled through the unified `libarchive.js` path, the built-in dialog asks for a password, and a correct password unlocks directory reading or nested entry previews.
|
|
33
34
|
- Uses `libarchive.js` Worker + WASM first to keep large archive parsing off the main thread.
|
|
34
|
-
- Falls back to ZIP / TAR / GZIP parsing when the Worker cannot be started, which helps mobile WebViews, local static servers, and private intranet deployments.
|
|
35
|
+
- Falls back to ZIP / TAR / GZIP parsing when the Worker cannot be started, which helps mobile WebViews, local static servers, and private intranet deployments. Encrypted archives never use the fallback path and require the libarchive Worker/WASM assets.
|
|
35
36
|
- Extracts internal files on demand, then delegates nested previews through `renderNestedBuffer` or the core dispatcher.
|
|
36
37
|
- Includes archive size limits, entry preview limits, worker timeout, IndexedDB cache, and single-entry download.
|
|
37
38
|
|
|
@@ -55,6 +56,24 @@ const options = {
|
|
|
55
56
|
}
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
## Encrypted Archives
|
|
60
|
+
|
|
61
|
+
By default, encrypted archives open a built-in password dialog. Applications can also provide an initial password or take over password collection:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const options = {
|
|
65
|
+
archive: {
|
|
66
|
+
password: initialPasswordFromYourSystem,
|
|
67
|
+
async requestPassword(context) {
|
|
68
|
+
// context.filename / context.entryName / context.reason / context.attempt
|
|
69
|
+
return await openYourPermissionCheckedPasswordModal(context)
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Return a string from `requestPassword` to continue. Return `null` or `undefined` to cancel and show a friendly notice. Wrong passwords request another password and never switch to the JSZip fallback.
|
|
76
|
+
|
|
58
77
|
## Migration Note
|
|
59
78
|
|
|
60
79
|
The core package no longer bundles the archive renderer and no longer installs `libarchive.js` for the archive pipeline. ZIP/TAR/GZIP fallback, `jszip`, cache, and Worker logic are owned by this package; core may still temporarily retain `jszip` for the OFD vendor path until OFD is fully extracted. Install this renderer explicitly, or use `@file-viewer/preset-all`, when archive preview is required.
|
package/README.md
CHANGED
|
@@ -30,8 +30,9 @@ const options = {
|
|
|
30
30
|
## 能力边界
|
|
31
31
|
|
|
32
32
|
- 支持 ZIP、TAR、GZIP、RAR、7z 等常见压缩包目录预览。
|
|
33
|
+
- 支持加密压缩包:检测到加密内容后使用 `libarchive.js` 统一解密,默认弹框要求用户输入密码,密码正确后继续读取目录或预览内部文件。
|
|
33
34
|
- 优先使用 `libarchive.js` Worker + WASM,避免大压缩包阻塞主线程。
|
|
34
|
-
- Worker 不可用时自动回退到 ZIP / TAR / GZIP 兼容模式,适合手机 WebView
|
|
35
|
+
- Worker 不可用时自动回退到 ZIP / TAR / GZIP 兼容模式,适合手机 WebView、本地临时服务器和内网静态部署排障;加密压缩包不会走 fallback,必须发布 libarchive Worker/WASM。
|
|
35
36
|
- 点击内部文件后才按需解压,并通过 `renderNestedBuffer` 或 core dispatcher 复用 PDF、Office、CAD、XMind、图片、代码等现有 renderer。
|
|
36
37
|
- 内置体积上限、单文件预览上限、Worker 超时、IndexedDB 缓存和下载入口,避免一次性把压缩包全部展开到内存。
|
|
37
38
|
|
|
@@ -55,6 +56,24 @@ const options = {
|
|
|
55
56
|
}
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
## 加密压缩包
|
|
60
|
+
|
|
61
|
+
默认情况下,检测到加密内容会显示内置密码弹框。业务也可以预置密码,或接管密码获取流程:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const options = {
|
|
65
|
+
archive: {
|
|
66
|
+
password: initialPasswordFromYourSystem,
|
|
67
|
+
async requestPassword(context) {
|
|
68
|
+
// context.filename / context.entryName / context.reason / context.attempt
|
|
69
|
+
return await openYourPermissionCheckedPasswordModal(context)
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`requestPassword` 返回字符串时继续解密;返回 `null` 或 `undefined` 时取消预览并展示友好提示。错误密码会重新请求密码,不会切换到 JSZip fallback。
|
|
76
|
+
|
|
58
77
|
## 迁移说明
|
|
59
78
|
|
|
60
79
|
`@file-viewer/core` 已不再内置 archive renderer,也不再为压缩包链路直接安装 `libarchive.js`。ZIP/TAR/GZIP fallback、`jszip`、缓存和 Worker 逻辑由本包维护;core 在 OFD 完全拆出前可能仍会因 OFD vendor 临时保留 `jszip`。需要压缩包预览时,请显式安装本包,或直接使用 `@file-viewer/preset-all` 聚合能力。
|
package/dist/archive.js
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
import { resolveFileViewerArchiveWasmUrl, resolveFileViewerArchiveWorkerUrl, } from '@file-viewer/core/assets';
|
|
2
|
-
import { createFileViewerTranslator, disposeFileViewerRendered, } from '@file-viewer/core';
|
|
2
|
+
import { collectFileViewerRendererPlugins, createFileRenderHandlerLoader, createFileViewerCoreRendererRegistry, createFileViewerTranslator, createRendererRegistry, disposeFileViewerRendered, installFileViewerRendererPlugins, listFileViewerAutoRendererPresets, normalizeSource, resolveFileViewerRendererPresetInputs, } from '@file-viewer/core';
|
|
3
3
|
import { buildArchiveNestedRenderContext, createArchiveCacheKey, flattenArchiveObject, formatArchiveBytes, getArchiveEntryExtension, } from './archiveShared.js';
|
|
4
4
|
import { readArchiveCache, writeArchiveCache } from './archiveCache.js';
|
|
5
|
-
import { loadArchiveEntriesWithoutWorker } from './archiveFallback.js';
|
|
5
|
+
import { isLikelyEncryptedArchive, loadArchiveEntriesWithoutWorker, } from './archiveFallback.js';
|
|
6
6
|
const DEFAULT_MAX_ARCHIVE_SIZE = 320 * 1024 * 1024;
|
|
7
7
|
const DEFAULT_MAX_ENTRY_PREVIEW_SIZE = 64 * 1024 * 1024;
|
|
8
8
|
const DEFAULT_WORKER_TIMEOUT_MS = 30000;
|
|
9
9
|
const MAX_LISTED_ENTRIES = 5000;
|
|
10
|
+
class ArchivePasswordCancelledError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ArchivePasswordCancelledError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
10
16
|
const archiveStyle = `
|
|
11
|
-
.archive-shell,.archive-viewer{position:relative;box-sizing:border-box;height:100%;min-height:0;display:grid;grid-template-columns:minmax(280px,34%) minmax(0,1fr);background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
17
|
+
.archive-shell,.archive-viewer{position:relative;box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;display:grid;grid-template-columns:minmax(280px,34%) minmax(0,1fr);background:#edf2f7;color:#172033;font-family:Aptos,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif}
|
|
12
18
|
.archive-shell *,.archive-viewer *{box-sizing:border-box}
|
|
13
|
-
.archive-sidebar{min-width:0;min-height:0;display:flex;flex-direction:column;gap:12px;padding:16px;border-right:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.72)}
|
|
19
|
+
.archive-sidebar{min-width:0;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:12px;padding:16px;border-right:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.72);transition:opacity .18s ease,padding .18s ease,border-color .18s ease}
|
|
20
|
+
.archive-shell.archive-sidebar-collapsed,.archive-viewer.archive-sidebar-collapsed{grid-template-columns:0 minmax(0,1fr)}
|
|
21
|
+
.archive-sidebar-collapsed .archive-sidebar{display:none;width:0;max-width:0;padding:0;border-color:transparent;opacity:0;pointer-events:none}
|
|
22
|
+
.archive-sidebar-collapsed .archive-sidebar>*{visibility:hidden}
|
|
23
|
+
.archive-head{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:10px;align-items:start}
|
|
24
|
+
.archive-head-main{min-width:0}
|
|
14
25
|
.archive-head span,.archive-preview-toolbar span{color:#6c7c90;font-size:12px;font-weight:800;letter-spacing:0}
|
|
15
26
|
.archive-head strong,.archive-preview-toolbar strong{display:block;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:18px;line-height:1.25}
|
|
16
|
-
.archive-head p{margin:8px 0 0;color:#69798b;font-size:13px}
|
|
27
|
+
.archive-head p{min-width:0;margin:8px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#69798b;font-size:13px}
|
|
28
|
+
.archive-sidebar-toggle{width:34px;height:34px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid rgba(23,32,51,.1);border-radius:10px;background:#fff;color:#1f7a58;font:inherit;font-size:17px;font-weight:900;line-height:1;cursor:pointer;box-shadow:0 6px 16px rgba(23,32,51,.07)}
|
|
29
|
+
.archive-sidebar-toggle:hover{border-color:rgba(31,122,88,.32);background:#f0fdf4}
|
|
17
30
|
.archive-warning,.archive-info,.archive-error{border-radius:12px;padding:10px 12px;background:#fff7e8;color:#8a4b00;font-size:13px;line-height:1.5}
|
|
18
31
|
.archive-info{background:#ecfdf5;color:#166534}
|
|
19
32
|
.archive-search{width:100%;height:42px;padding:0 12px;border-radius:12px;border:1px solid rgba(23,32,51,.1);outline:none;background:#fff;color:#172033;font:inherit}
|
|
20
|
-
.archive-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:7px;padding-right:4px}
|
|
21
|
-
.archive-entry{width:100%;min-height:58px;display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:10px;align-items:center;padding:8px 10px 8px calc(10px + var(--entry-depth,0) * 10px);border:1px solid rgba(23,32,51,.07);border-radius:12px;background:rgba(255,255,255,.86);color:inherit;text-align:left;cursor:pointer;font:inherit}
|
|
33
|
+
.archive-list{flex:1;min-width:0;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:7px;padding-right:4px}
|
|
34
|
+
.archive-entry{width:100%;min-width:0;min-height:58px;display:grid;grid-template-columns:42px minmax(0,1fr) minmax(46px,auto);gap:10px;align-items:center;padding:8px 10px 8px calc(10px + var(--entry-depth,0) * 10px);border:1px solid rgba(23,32,51,.07);border-radius:12px;background:rgba(255,255,255,.86);color:inherit;text-align:left;cursor:pointer;font:inherit}
|
|
22
35
|
.archive-entry:hover,.archive-entry.active{border-color:rgba(33,129,95,.28);box-shadow:0 10px 22px rgba(23,32,51,.08)}
|
|
23
36
|
.entry-ext{height:34px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;background:rgba(33,129,95,.12);color:#1d7a56;font-size:11px;font-weight:900;text-transform:uppercase}
|
|
24
37
|
.entry-copy{min-width:0}
|
|
25
38
|
.entry-copy strong,.entry-copy em{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
26
39
|
.entry-copy em,.archive-entry small{color:#718096;font-size:12px;font-style:normal}
|
|
40
|
+
.archive-entry small{min-width:0;max-width:74px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
|
|
27
41
|
.archive-preview{min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
28
|
-
.archive-preview-toolbar{min-height:64px;display:flex;align-items:center;
|
|
42
|
+
.archive-preview-toolbar{min-width:0;min-height:64px;display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid rgba(23,32,51,.08);background:rgba(255,255,255,.76)}
|
|
29
43
|
.archive-preview-toolbar button{height:34px;border:0;border-radius:10px;padding:0 12px;background:#1f7a58;color:#fff;font:inherit;font-size:13px;font-weight:800;cursor:pointer}
|
|
44
|
+
.archive-preview-toolbar .archive-sidebar-toggle{background:#fff;color:#1f7a58;border:1px solid rgba(23,32,51,.1);padding:0}
|
|
45
|
+
.archive-preview-title{min-width:0;flex:1}
|
|
46
|
+
.archive-preview-toolbar .archive-download-button{flex:0 0 auto}
|
|
30
47
|
.archive-nested-target{position:relative;flex:1;min-height:0;overflow:auto}
|
|
31
48
|
.archive-nested-content{width:100%;height:100%;min-height:420px}
|
|
32
49
|
.archive-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px;text-align:center;color:#64748b}
|
|
@@ -36,14 +53,28 @@ const archiveStyle = `
|
|
|
36
53
|
.archive-state p{margin:4px 0 0;color:#64748b}
|
|
37
54
|
.archive-spinner{width:34px;height:34px;flex-shrink:0;border-radius:999px;border:3px solid rgba(31,122,88,.16);border-top-color:#1f7a58;animation:archive-spin .9s linear infinite}
|
|
38
55
|
.archive-error{position:absolute;right:18px;bottom:18px;width:min(460px,calc(100% - 36px));box-shadow:0 16px 36px rgba(23,32,51,.14);z-index:5}
|
|
56
|
+
.archive-password-dialog{position:absolute;inset:0;z-index:8;display:flex;align-items:center;justify-content:center;padding:22px;background:rgba(15,23,42,.42);backdrop-filter:blur(10px)}
|
|
57
|
+
.archive-password-card{width:min(420px,100%);display:flex;flex-direction:column;gap:14px;padding:20px;border-radius:18px;background:#fff;color:#172033;box-shadow:0 24px 56px rgba(15,23,42,.24)}
|
|
58
|
+
.archive-password-card h3{margin:0;font-size:19px;line-height:1.25}
|
|
59
|
+
.archive-password-card p{margin:0;color:#64748b;font-size:13px;line-height:1.55}
|
|
60
|
+
.archive-password-card input{width:100%;height:44px;border-radius:12px;border:1px solid rgba(23,32,51,.12);outline:none;padding:0 12px;background:#fff;color:#172033;font:inherit}
|
|
61
|
+
.archive-password-card input:focus{border-color:rgba(31,122,88,.48);box-shadow:0 0 0 3px rgba(31,122,88,.12)}
|
|
62
|
+
.archive-password-error{min-height:18px;color:#b42318!important}
|
|
63
|
+
.archive-password-actions{display:flex;justify-content:flex-end;gap:10px}
|
|
64
|
+
.archive-password-actions button{height:38px;border:0;border-radius:10px;padding:0 14px;font:inherit;font-weight:800;cursor:pointer}
|
|
65
|
+
.archive-password-cancel{background:#eef2f7;color:#334155}
|
|
66
|
+
.archive-password-submit{background:#1f7a58;color:#fff}
|
|
39
67
|
.archive-hidden{display:none!important}
|
|
40
68
|
.file-viewer[data-viewer-theme='dark'] .archive-shell,.file-viewer[data-viewer-theme='dark'] .archive-viewer{background:#101820;color:#e6edf3}
|
|
41
69
|
.file-viewer[data-viewer-theme='dark'] .archive-sidebar,.file-viewer[data-viewer-theme='dark'] .archive-preview-toolbar{border-color:rgba(139,148,158,.2);background:rgba(21,27,35,.82)}
|
|
42
70
|
.file-viewer[data-viewer-theme='dark'] .archive-entry,.file-viewer[data-viewer-theme='dark'] .archive-search,.file-viewer[data-viewer-theme='dark'] .archive-state>div{background:#151b23;color:#e6edf3;border-color:rgba(139,148,158,.2)}
|
|
71
|
+
.file-viewer[data-viewer-theme='dark'] .archive-password-card{background:#151b23;color:#e6edf3}
|
|
72
|
+
.file-viewer[data-viewer-theme='dark'] .archive-password-card input{background:#0d1117;color:#e6edf3;border-color:rgba(139,148,158,.24)}
|
|
73
|
+
.file-viewer[data-viewer-theme='dark'] .archive-password-cancel{background:#212a35;color:#d7dee8}
|
|
43
74
|
.file-viewer[data-viewer-theme='dark'] .archive-empty strong{color:#f8fafc}
|
|
44
|
-
@media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .archive-shell,.file-viewer[data-viewer-theme='system'] .archive-viewer{background:#101820;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .archive-sidebar,.file-viewer[data-viewer-theme='system'] .archive-preview-toolbar{border-color:rgba(139,148,158,.2);background:rgba(21,27,35,.82)}.file-viewer[data-viewer-theme='system'] .archive-entry,.file-viewer[data-viewer-theme='system'] .archive-search,.file-viewer[data-viewer-theme='system'] .archive-state>div{background:#151b23;color:#e6edf3;border-color:rgba(139,148,158,.2)}.file-viewer[data-viewer-theme='system'] .archive-empty strong{color:#f8fafc}}
|
|
75
|
+
@media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .archive-shell,.file-viewer[data-viewer-theme='system'] .archive-viewer{background:#101820;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .archive-sidebar,.file-viewer[data-viewer-theme='system'] .archive-preview-toolbar{border-color:rgba(139,148,158,.2);background:rgba(21,27,35,.82)}.file-viewer[data-viewer-theme='system'] .archive-entry,.file-viewer[data-viewer-theme='system'] .archive-search,.file-viewer[data-viewer-theme='system'] .archive-state>div{background:#151b23;color:#e6edf3;border-color:rgba(139,148,158,.2)}.file-viewer[data-viewer-theme='system'] .archive-password-card{background:#151b23;color:#e6edf3}.file-viewer[data-viewer-theme='system'] .archive-password-card input{background:#0d1117;color:#e6edf3;border-color:rgba(139,148,158,.24)}.file-viewer[data-viewer-theme='system'] .archive-password-cancel{background:#212a35;color:#d7dee8}.file-viewer[data-viewer-theme='system'] .archive-empty strong{color:#f8fafc}}
|
|
45
76
|
@keyframes archive-spin{to{transform:rotate(360deg)}}
|
|
46
|
-
@media (max-width:860px){.archive-shell,.archive-viewer{grid-template-columns:1fr;grid-template-rows:minmax(220px,38%) minmax(0,1fr)}.archive-sidebar{border-right:0;border-bottom:1px solid rgba(23,32,51,.08)}}
|
|
77
|
+
@media (max-width:860px){.archive-shell,.archive-viewer{grid-template-columns:1fr;grid-template-rows:minmax(220px,38%) minmax(0,1fr)}.archive-shell.archive-sidebar-collapsed,.archive-viewer.archive-sidebar-collapsed{grid-template-columns:1fr;grid-template-rows:0 minmax(0,1fr)}.archive-sidebar{border-right:0;border-bottom:1px solid rgba(23,32,51,.08)}.archive-preview-toolbar{min-height:56px;padding:10px 12px}.archive-head strong,.archive-preview-toolbar strong{font-size:16px}.archive-entry{grid-template-columns:38px minmax(0,1fr) minmax(40px,auto)}}
|
|
47
78
|
`;
|
|
48
79
|
const createStyle = (documentRef) => {
|
|
49
80
|
const style = documentRef.createElement('style');
|
|
@@ -66,6 +97,62 @@ const normalizeWorkerError = (reason) => {
|
|
|
66
97
|
}
|
|
67
98
|
return typeof reason === 'string' ? reason : JSON.stringify(reason);
|
|
68
99
|
};
|
|
100
|
+
const isArchivePasswordError = (reason) => {
|
|
101
|
+
const message = normalizeWorkerError(reason).toLowerCase();
|
|
102
|
+
return /passphrase|password|encrypted|decrypt|crypto|wrong key|incorrect/i.test(message);
|
|
103
|
+
};
|
|
104
|
+
const resolveAutoRenderersEnabled = (options = {}) => {
|
|
105
|
+
const setting = options.autoRenderers;
|
|
106
|
+
if (typeof setting === 'boolean') {
|
|
107
|
+
return setting;
|
|
108
|
+
}
|
|
109
|
+
if ((setting === null || setting === void 0 ? void 0 : setting.enabled) !== undefined) {
|
|
110
|
+
return setting.enabled;
|
|
111
|
+
}
|
|
112
|
+
return (options.rendererMode || 'extend') !== 'replace';
|
|
113
|
+
};
|
|
114
|
+
const createNestedRendererRegistry = async (context) => {
|
|
115
|
+
const options = (context === null || context === void 0 ? void 0 : context.options) || {};
|
|
116
|
+
const registry = options.rendererMode === 'replace'
|
|
117
|
+
? createRendererRegistry([])
|
|
118
|
+
: createFileViewerCoreRendererRegistry({
|
|
119
|
+
builtinRenderers: options.builtinRenderers,
|
|
120
|
+
}).registry;
|
|
121
|
+
const rendererInputs = [];
|
|
122
|
+
if (resolveAutoRenderersEnabled(options)) {
|
|
123
|
+
rendererInputs.push(...listFileViewerAutoRendererPresets());
|
|
124
|
+
}
|
|
125
|
+
rendererInputs.push(...resolveFileViewerRendererPresetInputs(options.preset), ...resolveFileViewerRendererPresetInputs(options.presets));
|
|
126
|
+
if (options.renderers) {
|
|
127
|
+
rendererInputs.push(options.renderers);
|
|
128
|
+
}
|
|
129
|
+
const plugins = collectFileViewerRendererPlugins(rendererInputs);
|
|
130
|
+
if (!plugins.length) {
|
|
131
|
+
return registry;
|
|
132
|
+
}
|
|
133
|
+
await installFileViewerRendererPlugins({
|
|
134
|
+
registry,
|
|
135
|
+
plugins,
|
|
136
|
+
registerHandler: registration => {
|
|
137
|
+
const definition = registry.getById(registration.rendererId);
|
|
138
|
+
if (!definition) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
registry.register({
|
|
142
|
+
...definition,
|
|
143
|
+
load: createFileRenderHandlerLoader({
|
|
144
|
+
handler: registration.handler,
|
|
145
|
+
getTarget: loadContext => loadContext.surface.container,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return registry;
|
|
151
|
+
};
|
|
152
|
+
const createNestedRenderedInstance = (target, session) => ({
|
|
153
|
+
$el: target,
|
|
154
|
+
destroy: () => { var _a; return (_a = session.destroy) === null || _a === void 0 ? void 0 : _a.call(session); },
|
|
155
|
+
});
|
|
69
156
|
const withTimeout = async (promise, timeout, message, targetWindow) => {
|
|
70
157
|
let timer = 0;
|
|
71
158
|
const timerWindow = targetWindow || (typeof window !== 'undefined' ? window : undefined);
|
|
@@ -190,17 +277,36 @@ const resolveWorkerCandidates = async (documentRef, options) => {
|
|
|
190
277
|
}
|
|
191
278
|
return candidates;
|
|
192
279
|
};
|
|
193
|
-
const
|
|
280
|
+
const renderNestedWithCurrentOptions = async (buffer, type, target, context) => {
|
|
194
281
|
const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
if (!
|
|
282
|
+
const registry = await createNestedRendererRegistry(context);
|
|
283
|
+
const renderer = registry.getByExtension(type);
|
|
284
|
+
if (!(renderer === null || renderer === void 0 ? void 0 : renderer.load)) {
|
|
198
285
|
target.textContent = t('archive.error.nestedUnsupported', { type });
|
|
199
286
|
return undefined;
|
|
200
287
|
}
|
|
201
|
-
|
|
288
|
+
const session = await renderer.load({
|
|
289
|
+
source: normalizeSource({
|
|
290
|
+
buffer,
|
|
291
|
+
filename: (context === null || context === void 0 ? void 0 : context.filename) || `preview.${type}`,
|
|
292
|
+
type,
|
|
293
|
+
url: context === null || context === void 0 ? void 0 : context.url,
|
|
294
|
+
}),
|
|
295
|
+
surface: { container: target },
|
|
296
|
+
options: (context === null || context === void 0 ? void 0 : context.options) || {},
|
|
297
|
+
registerExportAdapter: context === null || context === void 0 ? void 0 : context.registerExportAdapter,
|
|
298
|
+
renderContext: {
|
|
299
|
+
...context,
|
|
300
|
+
renderNestedBuffer: (context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || renderNestedWithCurrentOptions,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return createNestedRenderedInstance(target, session);
|
|
304
|
+
};
|
|
305
|
+
const renderNestedEntry = (buffer, type, target, context) => {
|
|
306
|
+
const nestedRender = (context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || renderNestedWithCurrentOptions;
|
|
307
|
+
return nestedRender(buffer, type, target, {
|
|
202
308
|
...context,
|
|
203
|
-
renderNestedBuffer: (context === null || context === void 0 ? void 0 : context.renderNestedBuffer) ||
|
|
309
|
+
renderNestedBuffer: (context === null || context === void 0 ? void 0 : context.renderNestedBuffer) || renderNestedWithCurrentOptions,
|
|
204
310
|
});
|
|
205
311
|
};
|
|
206
312
|
export default async function renderArchive(buffer, target, _type, context) {
|
|
@@ -227,14 +333,22 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
227
333
|
let archiveNotice = '';
|
|
228
334
|
let encrypted = null;
|
|
229
335
|
let filterText = '';
|
|
336
|
+
let passwordAttempt = 0;
|
|
337
|
+
let passwordResolver = null;
|
|
338
|
+
let sidebarCollapsed = false;
|
|
230
339
|
const style = createStyle(documentRef);
|
|
231
340
|
const root = createElement(documentRef, 'section', 'archive-shell archive-viewer');
|
|
232
341
|
const sidebar = createElement(documentRef, 'aside', 'archive-sidebar');
|
|
233
342
|
const head = createElement(documentRef, 'div', 'archive-head');
|
|
343
|
+
const headMain = createElement(documentRef, 'div', 'archive-head-main');
|
|
234
344
|
const badge = createElement(documentRef, 'span', undefined, 'ARCHIVE');
|
|
235
345
|
const title = createElement(documentRef, 'strong', undefined, filename);
|
|
346
|
+
title.title = filename;
|
|
236
347
|
const stats = createElement(documentRef, 'p');
|
|
237
|
-
|
|
348
|
+
const sidebarHideButton = createElement(documentRef, 'button', 'archive-sidebar-toggle');
|
|
349
|
+
sidebarHideButton.type = 'button';
|
|
350
|
+
headMain.append(badge, title, stats);
|
|
351
|
+
head.append(headMain, sidebarHideButton);
|
|
238
352
|
const warning = createElement(documentRef, 'div', 'archive-warning');
|
|
239
353
|
const info = createElement(documentRef, 'div', 'archive-info');
|
|
240
354
|
const search = createElement(documentRef, 'input', 'archive-search');
|
|
@@ -245,11 +359,13 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
245
359
|
sidebar.append(head, warning, info, search, list);
|
|
246
360
|
const preview = createElement(documentRef, 'main', 'archive-preview');
|
|
247
361
|
const toolbar = createElement(documentRef, 'div', 'archive-preview-toolbar');
|
|
248
|
-
const
|
|
362
|
+
const sidebarShowButton = createElement(documentRef, 'button', 'archive-sidebar-toggle');
|
|
363
|
+
sidebarShowButton.type = 'button';
|
|
364
|
+
const toolbarTitle = createElement(documentRef, 'div', 'archive-preview-title');
|
|
249
365
|
toolbarTitle.append(createElement(documentRef, 'span', undefined, t('archive.preview.title')), createElement(documentRef, 'strong', undefined, t('archive.preview.chooseFile')));
|
|
250
|
-
const downloadButton = createElement(documentRef, 'button',
|
|
366
|
+
const downloadButton = createElement(documentRef, 'button', 'archive-download-button', t('archive.preview.downloadFile'));
|
|
251
367
|
downloadButton.type = 'button';
|
|
252
|
-
toolbar.append(toolbarTitle, downloadButton);
|
|
368
|
+
toolbar.append(sidebarShowButton, toolbarTitle, downloadButton);
|
|
253
369
|
const nestedTarget = createElement(documentRef, 'div', 'archive-nested-target');
|
|
254
370
|
preview.append(toolbar, nestedTarget);
|
|
255
371
|
root.append(sidebar, preview);
|
|
@@ -268,11 +384,105 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
268
384
|
const errorMessage = createElement(documentRef, 'p');
|
|
269
385
|
error.append(errorTitle, errorMessage);
|
|
270
386
|
root.append(error);
|
|
387
|
+
const passwordDialog = createElement(documentRef, 'div', 'archive-password-dialog archive-hidden');
|
|
388
|
+
passwordDialog.setAttribute('role', 'dialog');
|
|
389
|
+
passwordDialog.setAttribute('aria-modal', 'true');
|
|
390
|
+
const passwordCard = createElement(documentRef, 'form', 'archive-password-card');
|
|
391
|
+
const passwordTitle = createElement(documentRef, 'h3', undefined, t('archive.password.title'));
|
|
392
|
+
const passwordDescription = createElement(documentRef, 'p', undefined, t('archive.password.description'));
|
|
393
|
+
const passwordInput = createElement(documentRef, 'input');
|
|
394
|
+
passwordInput.type = 'password';
|
|
395
|
+
passwordInput.autocomplete = 'current-password';
|
|
396
|
+
passwordInput.placeholder = t('archive.password.placeholder');
|
|
397
|
+
const passwordError = createElement(documentRef, 'p', 'archive-password-error');
|
|
398
|
+
const passwordActions = createElement(documentRef, 'div', 'archive-password-actions');
|
|
399
|
+
const passwordCancelButton = createElement(documentRef, 'button', 'archive-password-cancel', t('archive.password.cancel'));
|
|
400
|
+
passwordCancelButton.type = 'button';
|
|
401
|
+
const passwordSubmitButton = createElement(documentRef, 'button', 'archive-password-submit', t('archive.password.confirm'));
|
|
402
|
+
passwordSubmitButton.type = 'submit';
|
|
403
|
+
passwordActions.append(passwordCancelButton, passwordSubmitButton);
|
|
404
|
+
passwordCard.append(passwordTitle, passwordDescription, passwordInput, passwordError, passwordActions);
|
|
405
|
+
passwordDialog.append(passwordCard);
|
|
406
|
+
root.append(passwordDialog);
|
|
271
407
|
target.replaceChildren(style, root);
|
|
272
408
|
const listen = (element, event, listener) => {
|
|
273
409
|
element.addEventListener(event, listener);
|
|
274
410
|
cleanups.push(() => element.removeEventListener(event, listener));
|
|
275
411
|
};
|
|
412
|
+
const closePasswordDialog = (password) => {
|
|
413
|
+
passwordDialog.classList.add('archive-hidden');
|
|
414
|
+
const resolve = passwordResolver;
|
|
415
|
+
passwordResolver = null;
|
|
416
|
+
resolve === null || resolve === void 0 ? void 0 : resolve(password);
|
|
417
|
+
};
|
|
418
|
+
const requestPasswordWithDialog = async (reason, previousError) => {
|
|
419
|
+
passwordDescription.textContent = t('archive.password.description');
|
|
420
|
+
passwordError.textContent = previousError || reason === 'invalid-password'
|
|
421
|
+
? t('archive.password.invalid')
|
|
422
|
+
: '';
|
|
423
|
+
passwordInput.value = '';
|
|
424
|
+
passwordDialog.classList.remove('archive-hidden');
|
|
425
|
+
targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.setTimeout(() => passwordInput.focus(), 0);
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
passwordResolver = resolve;
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
const requestArchivePassword = async (reason, entry, previousError) => {
|
|
431
|
+
passwordAttempt += 1;
|
|
432
|
+
const contextPayload = {
|
|
433
|
+
filename,
|
|
434
|
+
entryName: entry === null || entry === void 0 ? void 0 : entry.name,
|
|
435
|
+
attempt: passwordAttempt,
|
|
436
|
+
reason,
|
|
437
|
+
error: previousError,
|
|
438
|
+
};
|
|
439
|
+
if ((archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.password) && passwordAttempt === 1 && !previousError) {
|
|
440
|
+
return archiveOptions.password;
|
|
441
|
+
}
|
|
442
|
+
if (archiveOptions === null || archiveOptions === void 0 ? void 0 : archiveOptions.requestPassword) {
|
|
443
|
+
const customPassword = await archiveOptions.requestPassword(contextPayload);
|
|
444
|
+
return customPassword !== null && customPassword !== void 0 ? customPassword : null;
|
|
445
|
+
}
|
|
446
|
+
return requestPasswordWithDialog(reason, previousError);
|
|
447
|
+
};
|
|
448
|
+
const applyArchivePassword = async (password) => {
|
|
449
|
+
var _a;
|
|
450
|
+
await ((_a = archiveReader === null || archiveReader === void 0 ? void 0 : archiveReader.usePassword) === null || _a === void 0 ? void 0 : _a.call(archiveReader, password));
|
|
451
|
+
};
|
|
452
|
+
const requestAndApplyPassword = async (reason, entry, previousError) => {
|
|
453
|
+
const password = await requestArchivePassword(reason, entry, previousError);
|
|
454
|
+
if (password === null) {
|
|
455
|
+
throw new ArchivePasswordCancelledError(t('archive.error.passwordRequired'));
|
|
456
|
+
}
|
|
457
|
+
await applyArchivePassword(password);
|
|
458
|
+
};
|
|
459
|
+
listen(passwordCard, 'submit', (event) => {
|
|
460
|
+
event.preventDefault();
|
|
461
|
+
const password = passwordInput.value;
|
|
462
|
+
if (!password) {
|
|
463
|
+
passwordError.textContent = t('archive.password.required');
|
|
464
|
+
passwordInput.focus();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
closePasswordDialog(password);
|
|
468
|
+
});
|
|
469
|
+
listen(passwordCancelButton, 'click', () => {
|
|
470
|
+
closePasswordDialog(null);
|
|
471
|
+
});
|
|
472
|
+
listen(passwordDialog, 'keydown', (event) => {
|
|
473
|
+
if (event.key === 'Escape') {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
closePasswordDialog(null);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
listen(sidebarHideButton, 'click', () => {
|
|
479
|
+
sidebarCollapsed = true;
|
|
480
|
+
syncState();
|
|
481
|
+
});
|
|
482
|
+
listen(sidebarShowButton, 'click', () => {
|
|
483
|
+
sidebarCollapsed = !sidebarCollapsed;
|
|
484
|
+
syncState();
|
|
485
|
+
});
|
|
276
486
|
const getArchiveStats = () => {
|
|
277
487
|
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
278
488
|
const previewableCount = entries.filter(entry => entry.previewable).length;
|
|
@@ -299,6 +509,20 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
299
509
|
await ((_a = archiveReader === null || archiveReader === void 0 ? void 0 : archiveReader.close) === null || _a === void 0 ? void 0 : _a.call(archiveReader));
|
|
300
510
|
archiveReader = null;
|
|
301
511
|
};
|
|
512
|
+
const syncSidebarToggleState = () => {
|
|
513
|
+
const showLabel = t('archive.sidebar.show');
|
|
514
|
+
const hideLabel = t('archive.sidebar.hide');
|
|
515
|
+
const activeLabel = sidebarCollapsed ? showLabel : hideLabel;
|
|
516
|
+
root.classList.toggle('archive-sidebar-collapsed', sidebarCollapsed);
|
|
517
|
+
sidebarHideButton.textContent = '‹';
|
|
518
|
+
sidebarShowButton.textContent = sidebarCollapsed ? '☰' : '‹';
|
|
519
|
+
sidebarHideButton.title = hideLabel;
|
|
520
|
+
sidebarShowButton.title = activeLabel;
|
|
521
|
+
sidebarHideButton.setAttribute('aria-label', hideLabel);
|
|
522
|
+
sidebarShowButton.setAttribute('aria-label', activeLabel);
|
|
523
|
+
sidebarHideButton.setAttribute('aria-expanded', String(!sidebarCollapsed));
|
|
524
|
+
sidebarShowButton.setAttribute('aria-expanded', String(!sidebarCollapsed));
|
|
525
|
+
};
|
|
302
526
|
const syncState = () => {
|
|
303
527
|
const archiveStats = getArchiveStats();
|
|
304
528
|
stats.textContent = t('archive.stats.summary', {
|
|
@@ -319,7 +543,9 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
319
543
|
const activeTitle = toolbarTitle.querySelector('strong');
|
|
320
544
|
if (activeTitle) {
|
|
321
545
|
activeTitle.textContent = (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.name) || t('archive.preview.chooseFile');
|
|
546
|
+
activeTitle.setAttribute('title', (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.path) || (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.name) || '');
|
|
322
547
|
}
|
|
548
|
+
syncSidebarToggleState();
|
|
323
549
|
};
|
|
324
550
|
const renderEmptyState = () => {
|
|
325
551
|
if (selectedEntry || loading || nestedTarget.childElementCount) {
|
|
@@ -338,8 +564,16 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
338
564
|
button.classList.toggle('active', (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.id) === entry.id);
|
|
339
565
|
const icon = createElement(documentRef, 'span', 'entry-ext', entry.extension || 'file');
|
|
340
566
|
const copy = createElement(documentRef, 'span', 'entry-copy');
|
|
341
|
-
|
|
342
|
-
|
|
567
|
+
const nameNode = createElement(documentRef, 'strong', undefined, entry.name);
|
|
568
|
+
const pathNode = createElement(documentRef, 'em', undefined, entry.path);
|
|
569
|
+
const sizeText = formatArchiveBytes(entry.size);
|
|
570
|
+
const sizeNode = createElement(documentRef, 'small', undefined, sizeText);
|
|
571
|
+
nameNode.title = entry.name;
|
|
572
|
+
pathNode.title = entry.path;
|
|
573
|
+
sizeNode.title = sizeText;
|
|
574
|
+
button.title = entry.path;
|
|
575
|
+
copy.append(nameNode, pathNode);
|
|
576
|
+
button.append(icon, copy, sizeNode);
|
|
343
577
|
button.addEventListener('click', () => {
|
|
344
578
|
void previewEntry(entry);
|
|
345
579
|
});
|
|
@@ -365,6 +599,19 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
365
599
|
workers.forEach(worker => worker.terminate());
|
|
366
600
|
workers.length = 0;
|
|
367
601
|
};
|
|
602
|
+
const readArchiveDirectoryWithPassword = async (archive, candidate) => {
|
|
603
|
+
for (;;) {
|
|
604
|
+
try {
|
|
605
|
+
return await withTimeout(archive.getFilesObject(), workerTimeoutMs, t('archive.error.candidateReadTimeout', { label: candidate.label }), targetWindow);
|
|
606
|
+
}
|
|
607
|
+
catch (reason) {
|
|
608
|
+
if (!encrypted && !isArchivePasswordError(reason)) {
|
|
609
|
+
throw reason;
|
|
610
|
+
}
|
|
611
|
+
await requestAndApplyPassword(encrypted ? 'invalid-password' : 'read-failed', undefined, reason);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
368
615
|
const tryOpenArchiveWithWorker = async (Archive, candidate) => {
|
|
369
616
|
const createdWorkers = [];
|
|
370
617
|
const workerUrl = await prepareWorkerUrl(candidate, objectUrls);
|
|
@@ -382,8 +629,11 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
382
629
|
const archive = await withTimeout(Archive.open(archiveFile), workerTimeoutMs, t('archive.error.candidateInitTimeout', { label: candidate.label }), targetWindow);
|
|
383
630
|
archiveReader = archive;
|
|
384
631
|
encrypted = await withTimeout(archive.hasEncryptedData(), workerTimeoutMs, t('archive.error.encryptedCheckTimeout', { label: candidate.label }), targetWindow).catch(() => null);
|
|
632
|
+
if (encrypted) {
|
|
633
|
+
await requestAndApplyPassword('encrypted');
|
|
634
|
+
}
|
|
385
635
|
setLoading(true, t('archive.loading.readingDirectory'), t('archive.loading.directoryReadyHint'));
|
|
386
|
-
const fileTree = await
|
|
636
|
+
const fileTree = await readArchiveDirectoryWithPassword(archive, candidate);
|
|
387
637
|
entries = flattenArchiveObject(fileTree)
|
|
388
638
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
389
639
|
syncState();
|
|
@@ -436,10 +686,18 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
436
686
|
return;
|
|
437
687
|
}
|
|
438
688
|
catch (reason) {
|
|
689
|
+
if (reason instanceof ArchivePasswordCancelledError) {
|
|
690
|
+
throw reason;
|
|
691
|
+
}
|
|
439
692
|
errors.push(`${candidate.label}: ${normalizeWorkerError(reason)}`);
|
|
440
693
|
}
|
|
441
694
|
}
|
|
442
695
|
await closeArchive();
|
|
696
|
+
if (isLikelyEncryptedArchive(buffer, filename)) {
|
|
697
|
+
encrypted = true;
|
|
698
|
+
syncState();
|
|
699
|
+
throw new Error(t('archive.error.encryptedRequiresWorker'));
|
|
700
|
+
}
|
|
443
701
|
if (await tryOpenArchiveWithFallback()) {
|
|
444
702
|
return;
|
|
445
703
|
}
|
|
@@ -458,9 +716,7 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
458
716
|
const child = createElement(documentRef, 'div', 'archive-nested-content');
|
|
459
717
|
nestedTarget.append(child);
|
|
460
718
|
const nestedContext = buildArchiveNestedRenderContext(context, entry, archiveOptions);
|
|
461
|
-
nestedRendered = (
|
|
462
|
-
? await context.renderNestedBuffer(entryBuffer, entry.extension, child, nestedContext)
|
|
463
|
-
: await renderNestedWithCoreFallback(entryBuffer, entry.extension, child, nestedContext);
|
|
719
|
+
nestedRendered = await renderNestedEntry(entryBuffer, entry.extension, child, nestedContext);
|
|
464
720
|
};
|
|
465
721
|
const extractEntryBuffer = async (entry) => {
|
|
466
722
|
const cacheKey = createArchiveCacheKey(filename, buffer.byteLength, entry);
|
|
@@ -470,7 +726,21 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
470
726
|
return cached.buffer;
|
|
471
727
|
}
|
|
472
728
|
}
|
|
473
|
-
|
|
729
|
+
let file;
|
|
730
|
+
for (;;) {
|
|
731
|
+
try {
|
|
732
|
+
file = await entry.compressedFile.extract();
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
catch (reason) {
|
|
736
|
+
if (!archiveReader || !isArchivePasswordError(reason)) {
|
|
737
|
+
throw reason;
|
|
738
|
+
}
|
|
739
|
+
encrypted = true;
|
|
740
|
+
syncState();
|
|
741
|
+
await requestAndApplyPassword('extract-failed', entry, reason);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
474
744
|
const entryBuffer = await file.arrayBuffer();
|
|
475
745
|
if (cacheEnabled) {
|
|
476
746
|
await writeArchiveCache({
|
|
@@ -542,6 +812,7 @@ export default async function renderArchive(buffer, target, _type, context) {
|
|
|
542
812
|
return {
|
|
543
813
|
$el: root,
|
|
544
814
|
async unmount() {
|
|
815
|
+
closePasswordDialog(null);
|
|
545
816
|
cleanups.splice(0).forEach(cleanup => cleanup());
|
|
546
817
|
await clearNestedPreview();
|
|
547
818
|
await closeArchive();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ArchiveEntryView } from './archiveShared.js';
|
|
2
|
+
export declare const isLikelyEncryptedArchive: (data: ArrayBuffer, filename: string) => boolean;
|
|
2
3
|
/**
|
|
3
4
|
* Worker fallback for constrained browsers, temporary local servers, and
|
|
4
5
|
* mobile WebViews. The main libarchive path still covers broader formats;
|
package/dist/archiveFallback.js
CHANGED
|
@@ -2,6 +2,9 @@ import { getArchiveEntryExtension, isArchiveSystemMetadataPath, isPreviewableArc
|
|
|
2
2
|
const ZIP_LIKE_EXTENSIONS = new Set(['zip', 'zipx', 'jar', 'war', 'ear', 'apk', 'cbz']);
|
|
3
3
|
const TAR_LIKE_EXTENSIONS = new Set(['tar', 'tgz', 'gz', 'gzip']);
|
|
4
4
|
const TAR_BLOCK_SIZE = 512;
|
|
5
|
+
const ZIP_CENTRAL_FILE_HEADER = 0x02014b50;
|
|
6
|
+
const ZIP_LOCAL_FILE_HEADER = 0x04034b50;
|
|
7
|
+
const ZIP_GENERAL_PURPOSE_ENCRYPTED_FLAG = 0x0001;
|
|
5
8
|
const toArrayBuffer = (bytes) => {
|
|
6
9
|
const output = new Uint8Array(bytes.byteLength);
|
|
7
10
|
output.set(bytes);
|
|
@@ -97,6 +100,35 @@ const getArchiveExtension = (filename) => {
|
|
|
97
100
|
}
|
|
98
101
|
return getArchiveEntryExtension(filename);
|
|
99
102
|
};
|
|
103
|
+
const readUint16 = (view, offset) => {
|
|
104
|
+
return offset + 2 <= view.byteLength ? view.getUint16(offset, true) : 0;
|
|
105
|
+
};
|
|
106
|
+
const readUint32 = (view, offset) => {
|
|
107
|
+
return offset + 4 <= view.byteLength ? view.getUint32(offset, true) : 0;
|
|
108
|
+
};
|
|
109
|
+
const hasEncryptedZipEntries = (data) => {
|
|
110
|
+
const view = new DataView(data);
|
|
111
|
+
for (let offset = 0; offset + 12 <= view.byteLength; offset += 1) {
|
|
112
|
+
const signature = readUint32(view, offset);
|
|
113
|
+
if (signature !== ZIP_CENTRAL_FILE_HEADER && signature !== ZIP_LOCAL_FILE_HEADER) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const flagOffset = signature === ZIP_CENTRAL_FILE_HEADER
|
|
117
|
+
? offset + 8
|
|
118
|
+
: offset + 6;
|
|
119
|
+
if ((readUint16(view, flagOffset) & ZIP_GENERAL_PURPOSE_ENCRYPTED_FLAG) !== 0) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
export const isLikelyEncryptedArchive = (data, filename) => {
|
|
126
|
+
const extension = getArchiveExtension(filename);
|
|
127
|
+
if (ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
128
|
+
return hasEncryptedZipEntries(data);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
100
132
|
const getGzipEntryName = (filename) => {
|
|
101
133
|
const lower = filename.toLowerCase();
|
|
102
134
|
if (lower.endsWith('.gzip')) {
|
|
@@ -160,6 +192,9 @@ const loadTarEntries = async (data, filename, extension) => {
|
|
|
160
192
|
export const loadArchiveEntriesWithoutWorker = async (data, filename) => {
|
|
161
193
|
const extension = getArchiveExtension(filename);
|
|
162
194
|
if (ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
195
|
+
if (isLikelyEncryptedArchive(data, filename)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
163
198
|
return loadZipEntries(data);
|
|
164
199
|
}
|
|
165
200
|
if (TAR_LIKE_EXTENSIONS.has(extension)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/renderer-archive",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone archive renderer plugin for Flyfish File Viewer with libarchive worker, ZIP/TAR fallback, IndexedDB cache, and nested previews.",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"LICENSE"
|
|
57
57
|
],
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@file-viewer/core": "^2.1.
|
|
59
|
+
"@file-viewer/core": "^2.1.8",
|
|
60
60
|
"jszip": "^3.10.1",
|
|
61
61
|
"libarchive.js": "^2.0.2"
|
|
62
62
|
},
|