@arcships/light-ocr 0.3.4 → 0.4.0

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.
@@ -1,499 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- function adapterError(code, message, detail, cause) {
8
- const error = new Error(message, cause === undefined ? undefined : { cause });
9
- error.name = 'OcrError';
10
- error.code = code;
11
- if (detail) error.detail = detail;
12
- return error;
13
- }
14
-
15
- function platformIdentity() {
16
- const key = `${process.platform}-${process.arch}`;
17
- const identities = {
18
- 'darwin-arm64': { id: 'macos-arm64', os: 'darwin', architecture: 'arm64' },
19
- 'darwin-x64': { id: 'macos-x64', os: 'darwin', architecture: 'x86_64' },
20
- 'win32-arm64': { id: 'windows-arm64', os: 'win32', architecture: 'arm64' },
21
- 'win32-x64': { id: 'windows-x64', os: 'win32', architecture: 'x86_64' },
22
- };
23
- if (key === 'linux-x64') {
24
- const report = process.report?.getReport?.();
25
- if (report?.header?.glibcVersionRuntime) {
26
- return { id: 'linux-x64', os: 'linux', architecture: 'x86_64', libc: 'glibc' };
27
- }
28
- throw adapterError(
29
- 'unsupported_platform',
30
- 'light-ocr currently supports Linux x64 with glibc only',
31
- key,
32
- );
33
- }
34
- if (key === 'linux-arm64') {
35
- const report = process.report?.getReport?.();
36
- if (report?.header?.glibcVersionRuntime) {
37
- return { id: 'linux-arm64', os: 'linux', architecture: 'arm64', libc: 'glibc' };
38
- }
39
- throw adapterError(
40
- 'unsupported_platform',
41
- 'light-ocr currently supports Linux arm64 with glibc only',
42
- key,
43
- );
44
- }
45
- const identity = identities[key];
46
- if (!identity) {
47
- throw adapterError('unsupported_platform', `light-ocr does not support ${key}`, key);
48
- }
49
- return identity;
50
- }
51
-
52
- function platformPackage() {
53
- const packages = {
54
- 'macos-arm64': '@arcships/light-ocr-darwin-arm64',
55
- 'macos-x64': '@arcships/light-ocr-darwin-x64',
56
- 'windows-arm64': '@arcships/light-ocr-win32-arm64',
57
- 'windows-x64': '@arcships/light-ocr-win32-x64',
58
- 'linux-x64': '@arcships/light-ocr-linux-x64-gnu',
59
- 'linux-arm64': '@arcships/light-ocr-linux-arm64-gnu',
60
- };
61
- return packages[platformIdentity().id];
62
- }
63
-
64
- function exactKeys(value, expected, field) {
65
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
66
- throw adapterError('package_load_failed', `${field} must be an object`);
67
- }
68
- const actual = Object.keys(value).sort();
69
- const wanted = [...expected].sort();
70
- if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
71
- throw adapterError('package_load_failed', `${field} fields are invalid`);
72
- }
73
- }
74
-
75
- function safeArtifactPath(root, value, field) {
76
- if (typeof value !== 'string' || value === '' || value.includes('\0')) {
77
- throw adapterError('package_load_failed', `${field} must be a package-relative path`);
78
- }
79
- const normalized = value.replaceAll('\\', '/');
80
- if (
81
- path.posix.isAbsolute(normalized) ||
82
- /^[A-Za-z]:/.test(normalized) ||
83
- normalized.split('/').some((part) => part === '..' || part === '' || part === '.')
84
- ) {
85
- throw adapterError('package_load_failed', `${field} escapes the native package`, value);
86
- }
87
- const resolved = path.resolve(root, ...normalized.split('/'));
88
- const relative = path.relative(path.resolve(root), resolved);
89
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
90
- throw adapterError('package_load_failed', `${field} escapes the native package`, value);
91
- }
92
- return resolved;
93
- }
94
-
95
- function sha256(filename) {
96
- return crypto.createHash('sha256').update(fs.readFileSync(filename)).digest('hex');
97
- }
98
-
99
- function verifyArtifact(root, artifact, field) {
100
- exactKeys(artifact, ['path', 'bytes', 'sha256'], field);
101
- const filename = safeArtifactPath(root, artifact.path, `${field}.path`);
102
- let stats;
103
- try {
104
- stats = fs.lstatSync(filename);
105
- } catch (cause) {
106
- throw adapterError('package_load_failed', 'Descriptor artifact is missing', artifact.path, cause);
107
- }
108
- if (!stats.isFile() || stats.isSymbolicLink()) {
109
- throw adapterError('package_load_failed', 'Descriptor artifact is not a regular file', artifact.path);
110
- }
111
- if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || stats.size !== artifact.bytes) {
112
- throw adapterError('package_load_failed', 'Descriptor artifact byte count mismatch', artifact.path);
113
- }
114
- if (!/^[a-f0-9]{64}$/.test(artifact.sha256 || '') || sha256(filename) !== artifact.sha256) {
115
- throw adapterError('package_load_failed', 'Descriptor artifact hash mismatch', artifact.path);
116
- }
117
- return filename;
118
- }
119
-
120
- function sameArtifact(left, right) {
121
- return left?.path === right?.path && left?.bytes === right?.bytes &&
122
- left?.sha256 === right?.sha256;
123
- }
124
-
125
- function validateRuntimeDescriptor(descriptorPath) {
126
- const absoluteDescriptor = path.resolve(descriptorPath);
127
- const nativeDirectory = path.dirname(absoluteDescriptor);
128
- if (
129
- path.basename(absoluteDescriptor) !== 'runtime-descriptor.json' ||
130
- path.basename(nativeDirectory) !== 'native'
131
- ) {
132
- throw adapterError(
133
- 'package_load_failed',
134
- 'Native runtime descriptor must use the package native/runtime-descriptor.json path',
135
- absoluteDescriptor,
136
- );
137
- }
138
- let descriptor;
139
- try {
140
- const nativeStats = fs.lstatSync(nativeDirectory);
141
- if (!nativeStats.isDirectory() || nativeStats.isSymbolicLink()) {
142
- throw new Error('native payload root is not a regular directory');
143
- }
144
- const stats = fs.lstatSync(absoluteDescriptor);
145
- if (!stats.isFile() || stats.isSymbolicLink()) throw new Error('not a regular file');
146
- descriptor = JSON.parse(fs.readFileSync(absoluteDescriptor, 'utf8'));
147
- } catch (cause) {
148
- throw adapterError(
149
- 'package_load_failed',
150
- 'Unable to read the native runtime descriptor',
151
- absoluteDescriptor,
152
- cause,
153
- );
154
- }
155
- exactKeys(
156
- descriptor,
157
- ['schemaVersion', 'platform', 'runtime', 'qualificationOnly', 'released',
158
- 'autoPolicy', 'providers', 'addon'],
159
- 'runtime descriptor',
160
- );
161
- if (descriptor.schemaVersion !== '2.0') {
162
- throw adapterError('package_load_failed', 'Unsupported native runtime descriptor schema');
163
- }
164
- const root = path.dirname(path.dirname(absoluteDescriptor));
165
- const expected = platformIdentity();
166
- const expectedPlatformKeys = expected.libc
167
- ? ['id', 'os', 'architecture', 'libc']
168
- : ['id', 'os', 'architecture'];
169
- exactKeys(descriptor.platform, expectedPlatformKeys, 'platform');
170
- const actual = descriptor.platform;
171
- if (
172
- actual.id !== expected.id ||
173
- actual.os !== expected.os ||
174
- actual.architecture !== expected.architecture ||
175
- (expected.libc && actual.libc !== expected.libc)
176
- ) {
177
- throw adapterError('package_load_failed', 'Native runtime descriptor platform mismatch');
178
- }
179
- if (
180
- typeof descriptor.released !== 'boolean' ||
181
- typeof descriptor.qualificationOnly !== 'boolean' ||
182
- descriptor.qualificationOnly === descriptor.released
183
- ) {
184
- throw adapterError('package_load_failed', 'Native runtime descriptor release flags are invalid');
185
- }
186
-
187
- exactKeys(descriptor.autoPolicy, ['id', 'version', 'providers'], 'autoPolicy');
188
- const policy = descriptor.autoPolicy;
189
- if (
190
- typeof policy.id !== 'string' || policy.id === '' ||
191
- !Number.isSafeInteger(policy.version) || policy.version < 1 || policy.version > 0xffffffff ||
192
- !Array.isArray(policy.providers) || policy.providers.length === 0 ||
193
- policy.providers.length > 3 || policy.providers.at(-1) !== 'cpu' ||
194
- new Set(policy.providers).size !== policy.providers.length
195
- ) {
196
- throw adapterError('package_load_failed', 'Native runtime descriptor Auto policy is invalid');
197
- }
198
-
199
- exactKeys(descriptor.runtime, ['flavor', 'kind', 'version', 'abi', 'artifacts'], 'runtime');
200
- const runtime = descriptor.runtime;
201
- const expectedRuntimes = {
202
- cpu: { kind: 'onnxruntime-cpu', version: '1.22.0', abi: 'onnxruntime-c-api-22' },
203
- webgpu: {
204
- kind: 'onnxruntime-plugin-webgpu',
205
- version: '1.24.4',
206
- abi: 'onnxruntime-c-api-24-plugin-ep-0.1',
207
- },
208
- };
209
- const expectedRuntime = expectedRuntimes[runtime.flavor];
210
- if (
211
- !expectedRuntime || runtime.kind !== expectedRuntime.kind ||
212
- runtime.version !== expectedRuntime.version || runtime.abi !== expectedRuntime.abi ||
213
- !Array.isArray(runtime.artifacts) || runtime.artifacts.length === 0
214
- ) {
215
- throw adapterError('package_load_failed', 'Native runtime descriptor ABI identity is invalid');
216
- }
217
- if (runtime.flavor === 'webgpu' && !['linux', 'win32'].includes(actual.os)) {
218
- throw adapterError('package_load_failed', 'WebGPU runtime is not supported on this platform');
219
- }
220
- if (runtime.flavor !== 'webgpu' && descriptor.qualificationOnly) {
221
- throw adapterError('package_load_failed', 'CPU runtime cannot be qualification-only');
222
- }
223
-
224
- const addon = verifyArtifact(root, descriptor.addon, 'addon');
225
- const runtimePaths = new Set();
226
- const verifiedRuntime = new Map();
227
- runtime.artifacts.forEach((artifact, index) => {
228
- const filename = verifyArtifact(root, artifact, `runtime.artifacts[${index}]`);
229
- if (runtimePaths.has(artifact.path)) {
230
- throw adapterError('package_load_failed', 'Runtime artifact inventory contains a duplicate path');
231
- }
232
- runtimePaths.add(artifact.path);
233
- verifiedRuntime.set(artifact.path, filename);
234
- });
235
-
236
- exactKeys(descriptor.providers, Object.keys(descriptor.providers), 'providers');
237
- const availableProviders = Object.keys(descriptor.providers);
238
- if (
239
- availableProviders.length === 0 ||
240
- new Set(availableProviders).size !== availableProviders.length ||
241
- availableProviders.some((provider) => !['cpu', 'apple', 'webgpu'].includes(provider)) ||
242
- !descriptor.providers.cpu
243
- ) {
244
- throw adapterError('package_load_failed', 'Native runtime descriptor provider policy is invalid');
245
- }
246
- let webgpuLibrary = '';
247
- let webgpuProviderBytes = 0;
248
- let webgpuProviderSha256 = '';
249
- for (const [providerId, provider] of Object.entries(descriptor.providers)) {
250
- const expectedKeys = providerId === 'webgpu'
251
- ? ['runtimeProvider', 'providerVersion', 'qualificationId', 'providerLibrary', 'artifacts']
252
- : ['runtimeProvider', 'qualificationId', 'artifacts'];
253
- exactKeys(provider, expectedKeys, `providers.${providerId}`);
254
- if (
255
- typeof provider.qualificationId !== 'string' || provider.qualificationId === '' ||
256
- !Array.isArray(provider.artifacts) || provider.artifacts.length === 0
257
- ) {
258
- throw adapterError('package_load_failed', `Provider ${providerId} identity is invalid`);
259
- }
260
- const expectedProviderName = {
261
- cpu: 'CPUExecutionProvider',
262
- apple: 'CoreML',
263
- webgpu: 'WebGpuExecutionProvider',
264
- }[providerId];
265
- if (provider.runtimeProvider !== expectedProviderName) {
266
- throw adapterError('package_load_failed', `Provider ${providerId} runtime identity is invalid`);
267
- }
268
- const providerPaths = new Set();
269
- provider.artifacts.forEach((artifact, index) => {
270
- verifyArtifact(root, artifact, `providers.${providerId}.artifacts[${index}]`);
271
- if (providerPaths.has(artifact.path)) {
272
- throw adapterError('package_load_failed', `Provider ${providerId} has duplicate artifacts`);
273
- }
274
- providerPaths.add(artifact.path);
275
- if (providerId !== 'apple' && !runtimePaths.has(artifact.path)) {
276
- throw adapterError('package_load_failed', `Provider ${providerId} artifact is outside runtime inventory`);
277
- }
278
- if (providerId === 'apple' && artifact.path !== descriptor.addon.path) {
279
- throw adapterError('package_load_failed', 'Apple provider artifact must be the native addon');
280
- }
281
- });
282
- if (providerId === 'webgpu') {
283
- if (provider.providerVersion !== '0.1.0') {
284
- throw adapterError('package_load_failed', 'WebGPU provider version is invalid');
285
- }
286
- webgpuLibrary = verifyArtifact(root, provider.providerLibrary, 'providers.webgpu.providerLibrary');
287
- const declared = provider.artifacts.find(
288
- (artifact) => artifact.path === provider.providerLibrary.path,
289
- );
290
- const expectedBasename = actual.os === 'win32'
291
- ? 'onnxruntime_providers_webgpu.dll'
292
- : 'libonnxruntime_providers_webgpu.so';
293
- if (
294
- !declared || !sameArtifact(declared, provider.providerLibrary) ||
295
- path.basename(webgpuLibrary) !== expectedBasename
296
- ) {
297
- throw adapterError('package_load_failed', 'WebGPU provider library contract is invalid');
298
- }
299
- webgpuProviderBytes = provider.providerLibrary.bytes;
300
- webgpuProviderSha256 = provider.providerLibrary.sha256;
301
- }
302
- }
303
-
304
- const coreName = actual.os === 'win32'
305
- ? 'onnxruntime.dll'
306
- : actual.os === 'darwin'
307
- ? 'libonnxruntime.1.22.0.dylib'
308
- : 'libonnxruntime.so.1';
309
- const runtimeNames = [...verifiedRuntime.values()].map((filename) => path.basename(filename)).sort();
310
- const expectedRuntimeNames = runtime.flavor === 'webgpu'
311
- ? actual.os === 'win32'
312
- ? ['dxcompiler.dll', 'dxil.dll', 'onnxruntime.dll', 'onnxruntime_providers_webgpu.dll']
313
- : ['libonnxruntime.so.1', 'libonnxruntime_providers_webgpu.so']
314
- : [coreName];
315
- if (
316
- runtimeNames.length !== expectedRuntimeNames.length ||
317
- runtimeNames.some((name, index) => name !== expectedRuntimeNames[index])
318
- ) {
319
- throw adapterError('package_load_failed', 'Native runtime artifact set is incomplete');
320
- }
321
- const cpuArtifacts = descriptor.providers.cpu.artifacts;
322
- if (
323
- cpuArtifacts.length !== 1 ||
324
- path.basename(safeArtifactPath(root, cpuArtifacts[0].path, 'CPU artifact path')) !== coreName
325
- ) {
326
- throw adapterError('package_load_failed', 'CPU provider does not reference the core runtime');
327
- }
328
-
329
- const actualPayload = new Set();
330
- const inventory = (directory, relativeDirectory) => {
331
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
332
- const relative = `${relativeDirectory}/${entry.name}`;
333
- const filename = path.join(directory, entry.name);
334
- if (entry.isSymbolicLink()) {
335
- throw adapterError('package_load_failed', 'Native runtime payload contains a symlink', relative);
336
- }
337
- if (entry.isDirectory()) {
338
- inventory(filename, relative);
339
- } else if (entry.isFile() && relative !== 'native/runtime-descriptor.json') {
340
- actualPayload.add(relative);
341
- } else if (!entry.isFile()) {
342
- throw adapterError('package_load_failed', 'Native runtime payload is not a regular file', relative);
343
- }
344
- }
345
- };
346
- inventory(nativeDirectory, 'native');
347
- const referenced = new Set([descriptor.addon.path, ...runtimePaths]);
348
- if (
349
- actualPayload.size !== referenced.size ||
350
- [...actualPayload].some((filename) => !referenced.has(filename))
351
- ) {
352
- throw adapterError('package_load_failed', 'Native runtime descriptor payload inventory mismatch');
353
- }
354
-
355
- const expectedPolicy = runtime.flavor === 'webgpu'
356
- ? ['webgpu', 'cpu']
357
- : actual.id === 'macos-arm64'
358
- ? ['apple', 'cpu']
359
- : ['cpu'];
360
- const expectedAvailable = runtime.flavor === 'webgpu'
361
- ? ['cpu', 'webgpu']
362
- : actual.id === 'macos-arm64'
363
- ? ['apple', 'cpu']
364
- : ['cpu'];
365
- const sortedAvailable = [...availableProviders].sort();
366
- if (
367
- policy.providers.length !== expectedPolicy.length ||
368
- policy.providers.some((provider, index) => provider !== expectedPolicy[index]) ||
369
- sortedAvailable.length !== expectedAvailable.length ||
370
- sortedAvailable.some((provider, index) => provider !== expectedAvailable[index]) ||
371
- policy.providers.some((provider) => !availableProviders.includes(provider))
372
- ) {
373
- throw adapterError(
374
- 'package_load_failed',
375
- 'Native runtime descriptor providers disagree with platform capabilities',
376
- );
377
- }
378
- const providerQualificationIds = sortedAvailable.map(
379
- (providerId) => descriptor.providers[providerId].qualificationId,
380
- );
381
- const runtimePolicy = Object.freeze({
382
- id: policy.id,
383
- version: policy.version,
384
- platformId: actual.id,
385
- runtimeFlavor: runtime.flavor,
386
- runtimeVersion: runtime.version,
387
- runtimeAbi: runtime.abi,
388
- qualificationOnly: descriptor.qualificationOnly,
389
- released: descriptor.released,
390
- orderedCandidates: Object.freeze([...policy.providers]),
391
- availableProviders: Object.freeze(sortedAvailable),
392
- providerQualificationIds: Object.freeze(providerQualificationIds),
393
- webgpuProviderLibrary: webgpuLibrary,
394
- webgpuProviderBytes,
395
- webgpuProviderSha256,
396
- });
397
- return {
398
- addon,
399
- descriptor: Object.freeze(descriptor),
400
- descriptorPath: absoluteDescriptor,
401
- runtimePolicy,
402
- };
403
- }
404
-
405
- function resolveDevelopmentInput() {
406
- if (!process.env.LIGHT_OCR_NODE_BINARY) return undefined;
407
- const binary = path.resolve(process.env.LIGHT_OCR_NODE_BINARY);
408
- const descriptor = process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR
409
- ? path.resolve(process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR)
410
- : path.join(path.dirname(binary), 'runtime-descriptor.json');
411
- return { binary, descriptor };
412
- }
413
-
414
- function validateNativeContract(binding, runtimePolicy) {
415
- const contract = binding?.runtimeContract;
416
- const fields = [
417
- 'policyId',
418
- 'policyVersion',
419
- 'platformId',
420
- 'runtimeFlavor',
421
- 'runtimeVersion',
422
- 'runtimeAbi',
423
- 'qualificationOnly',
424
- 'released',
425
- ];
426
- const policyFields = {
427
- policyId: runtimePolicy.id,
428
- policyVersion: runtimePolicy.version,
429
- platformId: runtimePolicy.platformId,
430
- runtimeFlavor: runtimePolicy.runtimeFlavor,
431
- runtimeVersion: runtimePolicy.runtimeVersion,
432
- runtimeAbi: runtimePolicy.runtimeAbi,
433
- qualificationOnly: runtimePolicy.qualificationOnly,
434
- released: runtimePolicy.released,
435
- };
436
- if (
437
- !contract ||
438
- typeof contract !== 'object' ||
439
- fields.some((field) => contract[field] !== policyFields[field]) ||
440
- !Array.isArray(contract.orderedCandidates) ||
441
- !Array.isArray(contract.availableProviders) ||
442
- !Array.isArray(contract.providerQualificationIds) ||
443
- contract.orderedCandidates.length !== runtimePolicy.orderedCandidates.length ||
444
- contract.orderedCandidates.some(
445
- (provider, index) => provider !== runtimePolicy.orderedCandidates[index],
446
- ) ||
447
- contract.availableProviders.length !== runtimePolicy.availableProviders.length ||
448
- contract.availableProviders.some(
449
- (provider, index) => provider !== runtimePolicy.availableProviders[index],
450
- ) ||
451
- contract.providerQualificationIds.length !== runtimePolicy.providerQualificationIds.length ||
452
- contract.providerQualificationIds.some(
453
- (qualificationId, index) => qualificationId !== runtimePolicy.providerQualificationIds[index],
454
- )
455
- ) {
456
- throw adapterError(
457
- 'package_load_failed',
458
- 'Runtime descriptor is incompatible with the native addon ABI or capabilities',
459
- );
460
- }
461
- }
462
-
463
- function loadNative() {
464
- const development = resolveDevelopmentInput();
465
- let input;
466
- if (development) {
467
- input = development;
468
- } else {
469
- const packageName = platformPackage();
470
- try {
471
- const binary = require.resolve(packageName);
472
- input = { binary, descriptor: path.join(path.dirname(binary), 'runtime-descriptor.json') };
473
- } catch (cause) {
474
- throw adapterError(
475
- 'package_load_failed',
476
- `Unable to locate ${packageName}`,
477
- 'Reinstall @arcships/light-ocr without --omit=optional and verify that the current platform is supported.',
478
- cause,
479
- );
480
- }
481
- }
482
-
483
- if (!fs.existsSync(input.binary)) {
484
- throw adapterError('package_load_failed', 'Native addon is missing', input.binary);
485
- }
486
- const verified = validateRuntimeDescriptor(input.descriptor);
487
- if (path.resolve(input.binary) !== path.resolve(verified.addon)) {
488
- throw adapterError('package_load_failed', 'Runtime descriptor addon path mismatch', input.binary);
489
- }
490
- try {
491
- const binding = require(verified.addon);
492
- validateNativeContract(binding, verified.runtimePolicy);
493
- return Object.freeze({ binding, runtimePolicy: verified.runtimePolicy });
494
- } catch (cause) {
495
- throw adapterError('package_load_failed', 'Unable to load the verified native addon', '', cause);
496
- }
497
- }
498
-
499
- module.exports = { loadNative, validateRuntimeDescriptor };