@ankhorage/expo-runtime 3.1.1 → 3.2.2
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/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/dist/barcodeScanner.d.ts +2 -0
- package/dist/barcodeScanner.d.ts.map +1 -0
- package/dist/barcodeScanner.js +2 -0
- package/dist/barcodeScanner.js.map +1 -0
- package/dist/createExpoRuntimeRegistry.d.ts.map +1 -1
- package/dist/createExpoRuntimeRegistry.js +5 -0
- package/dist/createExpoRuntimeRegistry.js.map +1 -1
- package/dist/expoRuntimePlanningMetadata.d.ts +9 -1
- package/dist/expoRuntimePlanningMetadata.d.ts.map +1 -1
- package/dist/expoRuntimePlanningMetadata.js +8 -0
- package/dist/expoRuntimePlanningMetadata.js.map +1 -1
- package/dist/generatedSources.d.ts +1 -0
- package/dist/generatedSources.d.ts.map +1 -1
- package/dist/generatedSources.js +9 -1
- package/dist/generatedSources.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/planning.d.ts +1 -1
- package/dist/planning.d.ts.map +1 -1
- package/dist/planning.js +1 -1
- package/dist/planning.js.map +1 -1
- package/dist/reader/ExpoReaderSurfaceAdapter.d.ts +4 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.d.ts.map +1 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.js +102 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.js.map +1 -0
- package/dist/reader/ReaderDocumentError.d.ts +6 -0
- package/dist/reader/ReaderDocumentError.d.ts.map +1 -0
- package/dist/reader/ReaderDocumentError.js +9 -0
- package/dist/reader/ReaderDocumentError.js.map +1 -0
- package/dist/reader/ReaderViewport.d.ts +4 -0
- package/dist/reader/ReaderViewport.d.ts.map +1 -0
- package/dist/reader/ReaderViewport.js +212 -0
- package/dist/reader/ReaderViewport.js.map +1 -0
- package/dist/reader/drivers/EpubReaderDriver.d.ts +20 -0
- package/dist/reader/drivers/EpubReaderDriver.d.ts.map +1 -0
- package/dist/reader/drivers/EpubReaderDriver.js +259 -0
- package/dist/reader/drivers/EpubReaderDriver.js.map +1 -0
- package/dist/reader/drivers/PdfReaderDriver.d.ts +18 -0
- package/dist/reader/drivers/PdfReaderDriver.d.ts.map +1 -0
- package/dist/reader/drivers/PdfReaderDriver.js +198 -0
- package/dist/reader/drivers/PdfReaderDriver.js.map +1 -0
- package/dist/reader/epub/EpubArchive.d.ts +11 -0
- package/dist/reader/epub/EpubArchive.d.ts.map +1 -0
- package/dist/reader/epub/EpubArchive.js +65 -0
- package/dist/reader/epub/EpubArchive.js.map +1 -0
- package/dist/reader/epub/EpubArchiveFetcher.d.ts +11 -0
- package/dist/reader/epub/EpubArchiveFetcher.d.ts.map +1 -0
- package/dist/reader/epub/EpubArchiveFetcher.js +44 -0
- package/dist/reader/epub/EpubArchiveFetcher.js.map +1 -0
- package/dist/reader/epub/mediaTypes.d.ts +3 -0
- package/dist/reader/epub/mediaTypes.d.ts.map +1 -0
- package/dist/reader/epub/mediaTypes.js +26 -0
- package/dist/reader/epub/mediaTypes.js.map +1 -0
- package/dist/reader/epub/parseEpubPackage.d.ts +22 -0
- package/dist/reader/epub/parseEpubPackage.d.ts.map +1 -0
- package/dist/reader/epub/parseEpubPackage.js +98 -0
- package/dist/reader/epub/parseEpubPackage.js.map +1 -0
- package/dist/reader/epub/path.d.ts +4 -0
- package/dist/reader/epub/path.d.ts.map +1 -0
- package/dist/reader/epub/path.js +28 -0
- package/dist/reader/epub/path.js.map +1 -0
- package/dist/reader/epub/sanitizeEpubDocument.d.ts +3 -0
- package/dist/reader/epub/sanitizeEpubDocument.d.ts.map +1 -0
- package/dist/reader/epub/sanitizeEpubDocument.js +90 -0
- package/dist/reader/epub/sanitizeEpubDocument.js.map +1 -0
- package/dist/reader/index.d.ts +2 -0
- package/dist/reader/index.d.ts.map +1 -0
- package/dist/reader/index.js +2 -0
- package/dist/reader/index.js.map +1 -0
- package/dist/reader/loadReaderSourceAsync.d.ts +2 -0
- package/dist/reader/loadReaderSourceAsync.d.ts.map +1 -0
- package/dist/reader/loadReaderSourceAsync.js +49 -0
- package/dist/reader/loadReaderSourceAsync.js.map +1 -0
- package/dist/reader/readerViewportPresentation.d.ts +4 -0
- package/dist/reader/readerViewportPresentation.d.ts.map +1 -0
- package/dist/reader/readerViewportPresentation.js +26 -0
- package/dist/reader/readerViewportPresentation.js.map +1 -0
- package/dist/reader/types.d.ts +57 -0
- package/dist/reader/types.d.ts.map +1 -0
- package/dist/reader/types.js +2 -0
- package/dist/reader/types.js.map +1 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.d.ts +3 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.d.ts.map +1 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.js +48 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.js.map +1 -0
- package/package.json +25 -4
- package/src/adapterEntrypoints.test.ts +48 -0
- package/src/barcodeScanner.ts +1 -0
- package/src/createExpoRuntimeRegistry.test.tsx +9 -1
- package/src/createExpoRuntimeRegistry.tsx +7 -1
- package/src/expoRuntimePlanningMetadata.ts +9 -1
- package/src/generatedSources.test.ts +11 -2
- package/src/generatedSources.ts +11 -1
- package/src/index.ts +5 -1
- package/src/planning.ts +4 -1
- package/src/reader/ExpoReaderSurfaceAdapter.tsx +170 -0
- package/src/reader/ReaderDocumentError.ts +11 -0
- package/src/reader/ReaderViewport.tsx +319 -0
- package/src/reader/drivers/EpubReaderDriver.ts +309 -0
- package/src/reader/drivers/PdfReaderDriver.ts +253 -0
- package/src/reader/epub/EpubArchive.ts +83 -0
- package/src/reader/epub/EpubArchiveFetcher.ts +58 -0
- package/src/reader/epub/mediaTypes.ts +16 -0
- package/src/reader/epub/parseEpubPackage.test.ts +116 -0
- package/src/reader/epub/parseEpubPackage.ts +152 -0
- package/src/reader/epub/path.test.ts +22 -0
- package/src/reader/epub/path.ts +26 -0
- package/src/reader/epub/sanitizeEpubDocument.ts +121 -0
- package/src/reader/index.ts +1 -0
- package/src/reader/loadReaderSourceAsync.test.ts +52 -0
- package/src/reader/loadReaderSourceAsync.ts +59 -0
- package/src/reader/readerViewportPresentation.ts +28 -0
- package/src/reader/rendererPackages.test.ts +3 -1
- package/src/reader/types.ts +61 -0
- package/src/reader/validateEpubArchiveEntriesAsync.test.ts +142 -0
- package/src/reader/validateEpubArchiveEntriesAsync.ts +73 -0
- package/src/resolveExpoRuntimePlan.test.ts +36 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Entry } from '@zip.js/zip.js';
|
|
2
|
+
import {
|
|
3
|
+
ERR_OVERLAPPING_ENTRY,
|
|
4
|
+
TextReader,
|
|
5
|
+
Uint8ArrayReader,
|
|
6
|
+
Uint8ArrayWriter,
|
|
7
|
+
ZipReader,
|
|
8
|
+
ZipWriter,
|
|
9
|
+
} from '@zip.js/zip.js';
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
11
|
+
|
|
12
|
+
import { validateEpubArchiveEntriesAsync } from './validateEpubArchiveEntriesAsync';
|
|
13
|
+
|
|
14
|
+
describe('validateEpubArchiveEntriesAsync archive budgets', () => {
|
|
15
|
+
test('accepts a bounded archive and preflights every file without extracting it', async () => {
|
|
16
|
+
const entries = await createArchiveEntries([
|
|
17
|
+
['mimetype', 'application/epub+zip'],
|
|
18
|
+
['META-INF/container.xml', '<container />'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
expect(validateEpubArchiveEntriesAsync(entries)).resolves.toBeUndefined();
|
|
22
|
+
expect(entries.every((entry) => entry.directory || entry.localDirectory)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('rejects excessive entry counts before reading entry data', async () => {
|
|
26
|
+
const [entry] = await createArchiveEntries([['mimetype', 'application/epub+zip']]);
|
|
27
|
+
|
|
28
|
+
expect(validateEpubArchiveEntriesAsync(Array<Entry>(10_001).fill(entry))).rejects.toThrow(
|
|
29
|
+
'exceeds the 10000-entry limit',
|
|
30
|
+
);
|
|
31
|
+
expect(entry.localDirectory).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('rejects an oversized entry before reading entry data', async () => {
|
|
35
|
+
const [entry] = await createArchiveEntries([['chapter.xhtml', '<p>Chapter</p>']]);
|
|
36
|
+
entry.uncompressedSize = 64 * 1024 * 1024 + 1;
|
|
37
|
+
entry.compressedSize = entry.uncompressedSize;
|
|
38
|
+
|
|
39
|
+
expect(validateEpubArchiveEntriesAsync([entry])).rejects.toThrow(
|
|
40
|
+
'exceeds the 67108864-byte uncompressed limit',
|
|
41
|
+
);
|
|
42
|
+
expect(entry.localDirectory).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('rejects excessive aggregate expansion before reading entry data', async () => {
|
|
46
|
+
const entries = await createArchiveEntries(
|
|
47
|
+
Array.from({ length: 9 }, (_, index) => [`chapter-${index}.xhtml`, '<p>Chapter</p>']),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
entry.uncompressedSize = 64 * 1024 * 1024;
|
|
52
|
+
entry.compressedSize = entry.uncompressedSize;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(validateEpubArchiveEntriesAsync(entries)).rejects.toThrow(
|
|
56
|
+
'exceeds the 536870912-byte aggregate uncompressed limit',
|
|
57
|
+
);
|
|
58
|
+
expect(entries.every((entry) => !entry.localDirectory)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('rejects suspicious compression ratios before reading entry data', async () => {
|
|
62
|
+
const [entry] = await createArchiveEntries([['chapter.xhtml', '<p>Chapter</p>']]);
|
|
63
|
+
entry.uncompressedSize = 10_001;
|
|
64
|
+
entry.compressedSize = 100;
|
|
65
|
+
|
|
66
|
+
expect(validateEpubArchiveEntriesAsync([entry])).rejects.toThrow(
|
|
67
|
+
'exceeds the 100:1 compression-ratio limit',
|
|
68
|
+
);
|
|
69
|
+
expect(entry.localDirectory).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('validateEpubArchiveEntriesAsync archive structure', () => {
|
|
74
|
+
test('rejects invalid declared sizes before reading entry data', async () => {
|
|
75
|
+
const [entry] = await createArchiveEntries([['chapter.xhtml', '<p>Chapter</p>']]);
|
|
76
|
+
entry.compressedSize = Number.NaN;
|
|
77
|
+
|
|
78
|
+
expect(validateEpubArchiveEntriesAsync([entry])).rejects.toThrow('declares invalid sizes');
|
|
79
|
+
expect(entry.localDirectory).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('rejects overlapping entry ranges during the no-extraction preflight', async () => {
|
|
83
|
+
const archive = await createArchive([
|
|
84
|
+
['first.txt', 'same'],
|
|
85
|
+
['other.txt', 'same'],
|
|
86
|
+
]);
|
|
87
|
+
const centralDirectoryOffsets = findSignatureOffsets(archive, 0x02014b50);
|
|
88
|
+
const archiveView = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
89
|
+
const firstLocalHeaderOffset = archiveView.getUint32(centralDirectoryOffsets[0] + 42, true);
|
|
90
|
+
const firstCentralFilename = archive.slice(
|
|
91
|
+
centralDirectoryOffsets[0] + 46,
|
|
92
|
+
centralDirectoryOffsets[0] + 46 + 'first.txt'.length,
|
|
93
|
+
);
|
|
94
|
+
archive.set(firstCentralFilename, centralDirectoryOffsets[1] + 46);
|
|
95
|
+
archiveView.setUint32(centralDirectoryOffsets[1] + 42, firstLocalHeaderOffset, true);
|
|
96
|
+
const entries = await readArchiveEntries(archive, 'balanced');
|
|
97
|
+
|
|
98
|
+
expect(validateEpubArchiveEntriesAsync(entries)).rejects.toThrow(ERR_OVERLAPPING_ENTRY);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function createArchive(
|
|
103
|
+
files: readonly (readonly [filename: string, content: string])[],
|
|
104
|
+
): Promise<Uint8Array> {
|
|
105
|
+
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
106
|
+
|
|
107
|
+
for (const [filename, content] of files) {
|
|
108
|
+
await writer.add(filename, new TextReader(content));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return writer.close();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function createArchiveEntries(
|
|
115
|
+
files: readonly (readonly [filename: string, content: string])[],
|
|
116
|
+
): Promise<Entry[]> {
|
|
117
|
+
return readArchiveEntries(await createArchive(files));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findSignatureOffsets(data: Uint8Array, signature: number): number[] {
|
|
121
|
+
const offsets: number[] = [];
|
|
122
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
123
|
+
|
|
124
|
+
for (let offset = 0; offset <= data.byteLength - 4; offset += 1) {
|
|
125
|
+
if (view.getUint32(offset, true) === signature) {
|
|
126
|
+
offsets.push(offset);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return offsets;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readArchiveEntries(
|
|
134
|
+
data: Uint8Array,
|
|
135
|
+
strictness: 'balanced' | 'strict' = 'strict',
|
|
136
|
+
): Promise<Entry[]> {
|
|
137
|
+
const reader = new ZipReader(new Uint8ArrayReader(data), {
|
|
138
|
+
strictness,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return reader.getEntries({ strictness });
|
|
142
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Entry } from '@zip.js/zip.js';
|
|
2
|
+
import { Uint8ArrayWriter } from '@zip.js/zip.js';
|
|
3
|
+
|
|
4
|
+
const MAX_COMPRESSION_RATIO = 100;
|
|
5
|
+
const MAX_ENTRY_COUNT = 10_000;
|
|
6
|
+
const MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
const MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export async function validateEpubArchiveEntriesAsync(
|
|
10
|
+
entries: readonly Entry[],
|
|
11
|
+
signal?: AbortSignal,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (entries.length > MAX_ENTRY_COUNT) {
|
|
14
|
+
throw new Error(`EPUB archive exceeds the ${MAX_ENTRY_COUNT}-entry limit.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let totalUncompressedBytes = 0;
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
validateDeclaredSizes(entry);
|
|
21
|
+
|
|
22
|
+
if (entry.directory) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (entry.uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`EPUB archive entry "${entry.filename}" exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-byte uncompressed limit.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
entry.uncompressedSize > 0 &&
|
|
34
|
+
(entry.compressedSize === 0 ||
|
|
35
|
+
entry.uncompressedSize > entry.compressedSize * MAX_COMPRESSION_RATIO)
|
|
36
|
+
) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`EPUB archive entry "${entry.filename}" exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (totalUncompressedBytes > MAX_TOTAL_UNCOMPRESSED_BYTES - entry.uncompressedSize) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`EPUB archive exceeds the ${MAX_TOTAL_UNCOMPRESSED_BYTES}-byte aggregate uncompressed limit.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
totalUncompressedBytes += entry.uncompressedSize;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.directory) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await entry.getData(new Uint8ArrayWriter(), {
|
|
57
|
+
checkOverlappingEntryOnly: true,
|
|
58
|
+
signal,
|
|
59
|
+
strictness: 'strict',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateDeclaredSizes(entry: Entry): void {
|
|
65
|
+
if (
|
|
66
|
+
!Number.isSafeInteger(entry.compressedSize) ||
|
|
67
|
+
entry.compressedSize < 0 ||
|
|
68
|
+
!Number.isSafeInteger(entry.uncompressedSize) ||
|
|
69
|
+
entry.uncompressedSize < 0
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(`EPUB archive entry "${entry.filename}" declares invalid sizes.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -97,6 +97,42 @@ describe('resolveExpoRuntimePlan barcode capability', () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
describe('resolveExpoRuntimePlan ebook reader capability', () => {
|
|
101
|
+
test('selects the reader adapter and exact renderer packages without permissions', () => {
|
|
102
|
+
const plan = resolveExpoRuntimePlan(
|
|
103
|
+
withFirstScreenRequirements({ capabilities: [{ capability: 'ebookReader' }] }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(plan.permissions).toEqual([]);
|
|
107
|
+
expect(plan.impliedPermissions).toEqual([]);
|
|
108
|
+
expect(plan.providers).toEqual([]);
|
|
109
|
+
expect(plan.nativeConfig.plugins).toEqual([]);
|
|
110
|
+
expect(plan.runtimeAdapters).toEqual(['ExpoReaderSurfaceAdapter']);
|
|
111
|
+
expect(plan.dependencies.map(({ name, version }) => ({ name, version }))).toEqual([
|
|
112
|
+
{
|
|
113
|
+
name: '@ankhorage/expo-runtime',
|
|
114
|
+
version: GENERATED_RUNTIME_DEPENDENCY_VERSIONS['@ankhorage/expo-runtime'],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: '@readium/navigator',
|
|
118
|
+
version: GENERATED_RUNTIME_DEPENDENCY_VERSIONS['@readium/navigator'],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: '@readium/shared',
|
|
122
|
+
version: GENERATED_RUNTIME_DEPENDENCY_VERSIONS['@readium/shared'],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: '@zip.js/zip.js',
|
|
126
|
+
version: GENERATED_RUNTIME_DEPENDENCY_VERSIONS['@zip.js/zip.js'],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'pdfjs-dist',
|
|
130
|
+
version: GENERATED_RUNTIME_DEPENDENCY_VERSIONS['pdfjs-dist'],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
100
136
|
describe('resolveExpoRuntimePlan Expo 57 plugin coverage', () => {
|
|
101
137
|
test('keeps microphone-only native configuration free of background audio behavior', () => {
|
|
102
138
|
const plan = resolveExpoRuntimePlan(
|