Skip to content

Commit 880dcef

Browse files
authored
Merge pull request #24 from sonicbaume/zip-adapter-constructor
Encode zips using adapter
2 parents c303586 + 3b4dd81 commit 880dcef

19 files changed

Lines changed: 291 additions & 293 deletions

src/core/baseProcessor.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ import { AACTree, AACButton, AACSemanticCategory } from './treeStructure';
4444
import { StringCasing, detectCasing, isNumericOrEmpty } from './stringCasing';
4545
import { ValidationResult } from '../validation/validationTypes';
4646
import { BinaryOutput, ProcessorInput } from '../utils/io';
47-
import type { ZipAdapter } from '../utils/zip';
47+
import { getZipAdapter, ZipAdapter } from '../utils/zip';
4848

4949
// Configuration options for processors
50-
export interface ProcessorOptions {
50+
export interface ProcessorConfig {
5151
// Filter out navigation/system buttons (enabled by default)
5252
excludeNavigationButtons?: boolean;
5353
excludeSystemButtons?: boolean;
@@ -73,9 +73,10 @@ export interface ProcessorOptions {
7373
// Defaults to 'en-GB' if not specified
7474
grid3Locale?: string;
7575

76-
// Optionally provide your own adapter for unzipping the input
77-
zipAdapter?: (input: ProcessorInput) => Promise<{ zip: ZipAdapter }>;
76+
// Adapter for handling encoding/decoding zip files
77+
zipAdapter: (input?: ProcessorInput) => Promise<ZipAdapter>;
7878
}
79+
export type ProcessorOptions = Partial<ProcessorConfig>;
7980

8081
// Types for aac-tools-platform compatibility
8182
export interface ExtractedString {
@@ -117,14 +118,15 @@ export interface SourceString {
117118
}
118119

119120
abstract class BaseProcessor {
120-
protected options: ProcessorOptions;
121+
protected options: ProcessorConfig;
121122

122123
constructor(options: ProcessorOptions = {}) {
123124
// Default configuration: exclude navigation/system buttons
124125
this.options = {
125126
excludeNavigationButtons: true,
126127
excludeSystemButtons: true,
127128
preserveAllButtons: false,
129+
zipAdapter: getZipAdapter,
128130
...options,
129131
};
130132
}

src/processors/gridset/helpers.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { execSync } from 'child_process';
1212
import Database from 'better-sqlite3';
1313
import { dotNetTicksToDate } from '../../utils/dotnetTicks';
1414
import { getZipEntriesFromAdapter, resolveGridsetPasswordFromEnv } from './password';
15-
import { openZipFromInput, type ZipAdapter } from '../../utils/zip';
16-
import type { ProcessorInput } from '../../utils/io';
15+
import { ProcessorInput } from '../../utils/io';
16+
import { getZipAdapter, ZipAdapter } from '../../utils/zip';
1717

1818
function normalizeZipPath(p: string): string {
1919
const unified = p.replace(/\\/g, '/');
@@ -64,12 +64,10 @@ export async function openImage(
6464
gridsetBuffer: Uint8Array,
6565
entryPath: string,
6666
password = resolveGridsetPasswordFromEnv(),
67-
zipAdapter?: (input: ProcessorInput) => Promise<{ zip: ZipAdapter }>
67+
zipAdapter?: (input?: ProcessorInput) => Promise<ZipAdapter>
6868
): Promise<Uint8Array | null> {
6969
try {
70-
const { zip } = zipAdapter
71-
? await zipAdapter(gridsetBuffer)
72-
: await openZipFromInput(gridsetBuffer);
70+
const zip = zipAdapter ? await zipAdapter(gridsetBuffer) : await getZipAdapter(gridsetBuffer);
7371
const entries = getZipEntriesFromAdapter(zip, password);
7472
const want = normalizeZipPath(entryPath);
7573
const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);

src/processors/gridset/imageDebug.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* correctly in Grid3 gridsets.
66
*/
77

8-
import { openZipFromInput, type ZipAdapter } from '../../utils/zip';
98
import { getZipEntriesFromAdapter } from './password';
109
import { resolveGridsetPasswordFromEnv } from './password';
1110
import { XMLParser } from 'fast-xml-parser';
1211
import { decodeText, type ProcessorInput } from '../../utils/io';
12+
import { getZipAdapter, ZipAdapter } from '../../utils/zip';
1313

1414
export interface ImageIssue {
1515
gridName: string;
@@ -46,7 +46,7 @@ export interface ImageAuditResult {
4646
export async function auditGridsetImages(
4747
gridsetBuffer: Uint8Array,
4848
password = resolveGridsetPasswordFromEnv(),
49-
zipAdapter?: (input: ProcessorInput) => Promise<{ zip: ZipAdapter }>
49+
zipAdapter?: (input: ProcessorInput) => Promise<ZipAdapter>
5050
): Promise<ImageAuditResult> {
5151
const issues: ImageIssue[] = [];
5252
const availableImages = new Set<string>();
@@ -56,9 +56,7 @@ export async function auditGridsetImages(
5656
let unresolvedImages = 0;
5757

5858
try {
59-
const { zip } = zipAdapter
60-
? await zipAdapter(gridsetBuffer)
61-
: await openZipFromInput(gridsetBuffer);
59+
const zip = zipAdapter ? await zipAdapter(gridsetBuffer) : await getZipAdapter(gridsetBuffer);
6260

6361
const entries = getZipEntriesFromAdapter(zip, password);
6462
const parser = new XMLParser();

src/processors/gridset/password.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type JSZip from 'jszip';
22
import { ProcessorOptions } from '../../core/baseProcessor';
33
import { ProcessorInput } from '../../utils/io';
4-
import { type ZipAdapter } from '../../utils/zip';
4+
import { ZipAdapter } from '../../utils/zip';
55

66
function getExtension(source: string): string {
77
const index = source.lastIndexOf('.');

src/processors/gridset/symbolExtractor.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
*/
1414

1515
import * as fs from 'fs';
16-
import AdmZip from 'adm-zip';
1716
import { resolveSymbolReference, parseSymbolReference, type SymbolReference } from './symbols';
17+
import { ProcessorInput } from '../../utils/io';
18+
import { getZipAdapter, ZipAdapter } from '../../utils/zip';
1819

1920
/**
2021
* Image extraction result
@@ -88,21 +89,22 @@ const OPEN_LICENSE_SYMBOLS: {
8889
* @param options - Extraction options
8990
* @returns Extracted image data
9091
*/
91-
export function extractButtonImage(
92+
export async function extractButtonImage(
9293
gridsetBuffer: Buffer,
9394
resolvedImageEntry: string | undefined,
9495
symbolReference: string | undefined,
95-
options: SymbolExtractionOptions = {}
96-
): ExtractedImage {
96+
options: SymbolExtractionOptions = {},
97+
zipAdapter: (input: ProcessorInput) => Promise<ZipAdapter>
98+
): Promise<ExtractedImage> {
9799
// Priority 1: Use embedded image if available
98100
if (resolvedImageEntry && options.preferEmbedded !== false) {
99101
try {
100-
const zip = new AdmZip(gridsetBuffer);
101-
const entries = zip.getEntries();
102-
const entry = entries.find((e: any) => e.entryName === resolvedImageEntry);
102+
const zip = zipAdapter ? await zipAdapter(gridsetBuffer) : await getZipAdapter(gridsetBuffer);
103+
const entries = zip.listFiles();
104+
const entry = entries.find((e) => e === resolvedImageEntry);
103105

104106
if (entry) {
105-
const data = entry.getData();
107+
const data = Buffer.from(await zip.readFile(entry));
106108
const format = detectImageFormat(data);
107109
return {
108110
found: true,
@@ -119,7 +121,7 @@ export function extractButtonImage(
119121

120122
// Priority 2: Check symbol library reference
121123
if (symbolReference) {
122-
return extractSymbolLibraryImage(symbolReference, options);
124+
return await extractSymbolLibraryImage(symbolReference, options);
123125
}
124126

125127
// Not found
@@ -135,10 +137,10 @@ export function extractButtonImage(
135137
* @param options - Extraction options
136138
* @returns Extracted image or reference info
137139
*/
138-
export function extractSymbolLibraryImage(
140+
export async function extractSymbolLibraryImage(
139141
reference: string,
140142
options: SymbolExtractionOptions = {}
141-
): ExtractedImage {
143+
): Promise<ExtractedImage> {
142144
const ref = parseSymbolReferenceSafe(reference);
143145

144146
if (!ref || !ref.isValid) {
@@ -153,7 +155,7 @@ export function extractSymbolLibraryImage(
153155
const libInfo = OPEN_LICENSE_SYMBOLS[ref.library as keyof typeof OPEN_LICENSE_SYMBOLS];
154156

155157
// Resolve symbol reference and extract from .symbols file
156-
const resolved = resolveSymbolReference(reference, {
158+
const resolved = await resolveSymbolReference(reference, {
157159
grid3Path: options.grid3Path,
158160
});
159161

src/processors/gridset/symbols.ts

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
* This module provides symbol resolution and metadata extraction.
1414
*/
1515

16-
import { getFs, getNodeRequire, getPath } from '../../utils/io';
16+
import { getFs, getPath, ProcessorInput } from '../../utils/io';
17+
import { getZipAdapter, ZipAdapter } from '../../utils/zip';
1718

1819
/**
1920
* Default Grid 3 installation paths by platform
@@ -123,23 +124,6 @@ function getNodePath(): typeof import('path') {
123124
}
124125
}
125126

126-
let cachedAdmZip: typeof import('adm-zip') | null = null;
127-
function getAdmZip(): typeof import('adm-zip') {
128-
if (cachedAdmZip) return cachedAdmZip;
129-
try {
130-
const nodeRequire = getNodeRequire();
131-
// eslint-disable-next-line @typescript-eslint/no-var-requires
132-
const module = nodeRequire('adm-zip') as typeof import('adm-zip') & {
133-
default?: typeof import('adm-zip');
134-
};
135-
const resolved = module.default || module;
136-
cachedAdmZip = resolved;
137-
return resolved;
138-
} catch {
139-
throw new Error('Symbol library access requires AdmZip in this environment.');
140-
}
141-
}
142-
143127
/**
144128
* Parse a symbol reference string
145129
* @param reference - Symbol reference like "[widgit]/food/apple.png"
@@ -337,10 +321,11 @@ export function getSymbolLibraryInfo(
337321
* @param options - Resolution options
338322
* @returns Resolution result with image data if found
339323
*/
340-
export function resolveSymbolReference(
324+
export async function resolveSymbolReference(
341325
reference: string,
342-
options: SymbolResolutionOptions = {}
343-
): SymbolResolutionResult {
326+
options: SymbolResolutionOptions = {},
327+
zipAdapter?: (input: ProcessorInput) => Promise<ZipAdapter>
328+
): Promise<SymbolResolutionResult> {
344329
const parsed = parseSymbolReference(reference);
345330

346331
if (!parsed.isValid) {
@@ -373,19 +358,19 @@ export function resolveSymbolReference(
373358

374359
try {
375360
// .symbols files are ZIP archives
376-
const AdmZip = getAdmZip();
377-
const zip = new AdmZip(libraryInfo.pixFile);
361+
const zipFile = libraryInfo.pixFile;
362+
const zip = zipAdapter ? await zipAdapter(zipFile) : await getZipAdapter(zipFile);
378363

379364
// The path in the symbol reference becomes the path within the symbols/ folder
380365
// e.g., [tawasl]/above bw.png becomes symbols/above bw.png
381366
const symbolPath = `symbols/${parsed.path}`;
382367

383-
const entry = zip.getEntry(symbolPath);
368+
const entry = await zip.readFile(symbolPath);
384369

385370
if (!entry) {
386371
// Try without the symbols/ prefix (in case reference already includes it)
387372
const altPath = parsed.path.startsWith('symbols/') ? parsed.path : `symbols/${parsed.path}`;
388-
const altEntry = zip.getEntry(altPath);
373+
const altEntry = await zip.readFile(altPath);
389374

390375
if (!altEntry) {
391376
return {
@@ -398,7 +383,7 @@ export function resolveSymbolReference(
398383
}
399384

400385
// Found with alternate path
401-
const data = altEntry.getData();
386+
const data = Buffer.from(altEntry);
402387
return {
403388
reference: parsed,
404389
found: true,
@@ -409,7 +394,7 @@ export function resolveSymbolReference(
409394
}
410395

411396
// Found the symbol!
412-
const data = entry.getData();
397+
const data = Buffer.from(entry);
413398
return {
414399
reference: parsed,
415400
found: true,

src/processors/gridset/wordlistHelpers.ts

Lines changed: 14 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
1313
import { getZipEntriesFromAdapter, resolveGridsetPasswordFromEnv } from './password';
14-
import { openZipFromInput, type ZipAdapter } from '../../utils/zip';
15-
import { getNodeRequire, isNodeRuntime, type ProcessorInput } from '../../utils/io';
14+
import { type ProcessorInput } from '../../utils/io';
1615
import { decodeText } from '../../utils/io';
16+
import { getZipAdapter, ZipAdapter, ZipFile } from '../../utils/zip';
1717

1818
/**
1919
* Represents a single item in a wordlist
@@ -131,15 +131,13 @@ export function wordlistToXml(wordlist: WordList): string {
131131
export async function extractWordlists(
132132
gridsetBuffer: Uint8Array,
133133
password = resolveGridsetPasswordFromEnv(),
134-
zipAdapter?: (input: ProcessorInput) => Promise<{ zip: ZipAdapter }>
134+
zipAdapter?: (input: ProcessorInput) => Promise<ZipAdapter>
135135
): Promise<Map<string, WordList>> {
136136
const wordlists = new Map<string, WordList>();
137137
const parser = new XMLParser();
138138

139139
try {
140-
const { zip } = zipAdapter
141-
? await zipAdapter(gridsetBuffer)
142-
: await openZipFromInput(gridsetBuffer);
140+
const zip = zipAdapter ? await zipAdapter(gridsetBuffer) : await getZipAdapter(gridsetBuffer);
143141
const entries = getZipEntriesFromAdapter(zip, password);
144142

145143
// Process each grid file
@@ -212,7 +210,8 @@ export async function updateWordlist(
212210
gridsetBuffer: Uint8Array,
213211
gridName: string,
214212
wordlist: WordList,
215-
password = resolveGridsetPasswordFromEnv()
213+
password = resolveGridsetPasswordFromEnv(),
214+
zipAdapter?: (input: ProcessorInput) => Promise<ZipAdapter>
216215
): Promise<Uint8Array> {
217216
const parser = new XMLParser();
218217
const builder = new XMLBuilder({
@@ -222,50 +221,9 @@ export async function updateWordlist(
222221
suppressEmptyNode: false,
223222
});
224223

225-
let entries: Array<{
226-
entryName: string;
227-
getData: () => Promise<Uint8Array>;
228-
}>;
229-
let saveZip: (() => Promise<Uint8Array>) | null = null;
230-
let updateEntry: ((entryName: string, xml: string) => void) | null = null;
231-
232-
try {
233-
if (isNodeRuntime()) {
234-
const AdmZip = getNodeRequire()('adm-zip') as typeof import('adm-zip');
235-
const zip = new AdmZip(Buffer.from(gridsetBuffer));
236-
entries = zip.getEntries().map((entry) => ({
237-
entryName: entry.entryName,
238-
getData: () => Promise.resolve(entry.getData()),
239-
}));
240-
updateEntry = (entryName: string, xml: string) => {
241-
zip.addFile(entryName, Buffer.from(xml, 'utf8'));
242-
};
243-
saveZip = () => Promise.resolve(zip.toBuffer());
244-
} else {
245-
const module = await import('jszip');
246-
const JSZip = module.default || module;
247-
const zip = await JSZip.loadAsync(gridsetBuffer);
248-
entries = getZipEntriesFromAdapter(
249-
{
250-
listFiles: () => Object.keys(zip.files),
251-
readFile: async (name: string) => {
252-
const file = zip.file(name);
253-
if (!file) {
254-
throw new Error(`Zip entry not found: ${name}`);
255-
}
256-
return file.async('uint8array');
257-
},
258-
},
259-
password
260-
);
261-
updateEntry = (entryName: string, xml: string) => {
262-
zip.file(entryName, xml, { binary: false });
263-
};
264-
saveZip = async () => zip.generateAsync({ type: 'uint8array' });
265-
}
266-
} catch (error: any) {
267-
throw new Error(`Invalid gridset buffer: ${error.message}`);
268-
}
224+
const updatedEntries: ZipFile[] = [];
225+
const zip = zipAdapter ? await zipAdapter(gridsetBuffer) : await getZipAdapter(gridsetBuffer);
226+
const entries = getZipEntriesFromAdapter(zip, password);
269227

270228
let found = false;
271229

@@ -308,9 +266,10 @@ export async function updateWordlist(
308266

309267
// Rebuild the XML
310268
const updatedXml = builder.build(data);
311-
if (updateEntry) {
312-
updateEntry(entry.entryName, updatedXml);
313-
}
269+
updatedEntries.push({
270+
name: entry.entryName,
271+
data: updatedXml,
272+
});
314273
found = true;
315274
} catch (error) {
316275
const message = error instanceof Error ? error.message : String(error);
@@ -324,8 +283,5 @@ export async function updateWordlist(
324283
throw new Error(`Grid "${gridName}" not found in gridset`);
325284
}
326285

327-
if (!saveZip) {
328-
throw new Error('Failed to serialize updated gridset.');
329-
}
330-
return await saveZip();
286+
return await zip.writeFiles(updatedEntries);
331287
}

0 commit comments

Comments
 (0)