mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- Add backend selection, setup gating, Anki integration, and external host support - Add launcher flags, documentation, packaging, and focused tests - Open on-demand overlay modals on the first attempt
85 lines
2.6 KiB
JavaScript
85 lines
2.6 KiB
JavaScript
/*
|
|
* Reads only Yomitan index.json for an interactive import decision. The engine
|
|
* still imports and validates the complete archive before anything is published.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { BlobReader, Writer, ZipReader } from "./vendor/zip.js";
|
|
import { dictionaryArchiveIdentity } from "./dictionary-import.js";
|
|
|
|
export const MAX_PREFLIGHT_INDEX_BYTES = 1024 * 1024;
|
|
|
|
export class BoundedIndexWriter extends Writer {
|
|
constructor(limit) {
|
|
super();
|
|
this.limit = limit;
|
|
this.length = 0;
|
|
this.parts = [];
|
|
}
|
|
|
|
writeUint8Array(bytes) {
|
|
if (this.length + bytes.byteLength > this.limit) {
|
|
throw new Error(`The selected archive's index.json exceeds ${this.limit} bytes.`);
|
|
}
|
|
this.parts.push(bytes.slice());
|
|
this.length += bytes.byteLength;
|
|
}
|
|
|
|
getData() {
|
|
const data = new Uint8Array(this.length);
|
|
let offset = 0;
|
|
for (const part of this.parts) {
|
|
data.set(part, offset);
|
|
offset += part.byteLength;
|
|
}
|
|
return new TextDecoder().decode(data);
|
|
}
|
|
}
|
|
|
|
function assertRegularIndex(entry) {
|
|
const kind = entry.unixMode === undefined ? 0 : entry.unixMode & 0o170000;
|
|
if (entry.directory || entry.symlink || (kind !== 0 && kind !== 0o100000)) {
|
|
throw new Error("The selected archive's index.json is not a regular file.");
|
|
}
|
|
if (entry.encrypted) {
|
|
throw new Error("The selected archive's index.json is encrypted.");
|
|
}
|
|
}
|
|
|
|
export async function readDictionaryArchiveIdentity(blob) {
|
|
const reader = new ZipReader(new BlobReader(blob), {
|
|
useWebWorkers: false,
|
|
checkAmbiguity: true,
|
|
});
|
|
try {
|
|
const entries = await reader.getEntries();
|
|
const indexes = entries.filter(entry => entry.filename === "index.json");
|
|
if (indexes.length !== 1) {
|
|
throw new Error(indexes.length === 0
|
|
? "The selected archive has no index.json."
|
|
: "The selected archive has more than one index.json.");
|
|
}
|
|
const entry = indexes[0];
|
|
assertRegularIndex(entry);
|
|
if (entry.uncompressedSize > MAX_PREFLIGHT_INDEX_BYTES) {
|
|
throw new Error(`The selected archive's index.json exceeds ${MAX_PREFLIGHT_INDEX_BYTES} bytes.`);
|
|
}
|
|
const data = await entry.getData(new BoundedIndexWriter(MAX_PREFLIGHT_INDEX_BYTES), {
|
|
useWebWorkers: false,
|
|
checkSignature: true,
|
|
checkOverlappingEntry: true,
|
|
checkAmbiguity: true,
|
|
});
|
|
let index;
|
|
try {
|
|
index = JSON.parse(data);
|
|
} catch (error) {
|
|
throw new Error(`The selected archive's index.json is malformed: ${error.message || String(error)}`);
|
|
}
|
|
return dictionaryArchiveIdentity(index);
|
|
} finally {
|
|
await reader.close();
|
|
}
|
|
}
|