mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
feat(dictionary): add Hachidori backend support
- 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
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user