@ankhorage/reader 0.0.0-stage → 0.1.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/dist/features/reader/ReaderViewport.d.ts +4 -0
  3. package/dist/features/reader/ReaderViewport.d.ts.map +1 -0
  4. package/dist/features/reader/ReaderViewport.js +210 -0
  5. package/dist/features/reader/ReaderViewport.js.map +1 -0
  6. package/dist/features/reader/adapters/ReaderView.d.ts +4 -0
  7. package/dist/features/reader/adapters/ReaderView.d.ts.map +1 -0
  8. package/dist/features/reader/adapters/ReaderView.js +5 -0
  9. package/dist/features/reader/adapters/ReaderView.js.map +1 -0
  10. package/dist/features/reader/adapters/ReaderView.native.d.ts +5 -0
  11. package/dist/features/reader/adapters/ReaderView.native.d.ts.map +1 -0
  12. package/dist/features/reader/adapters/ReaderView.native.js +22 -0
  13. package/dist/features/reader/adapters/ReaderView.native.js.map +1 -0
  14. package/dist/features/reader/adapters/ReaderView.web.d.ts +5 -0
  15. package/dist/features/reader/adapters/ReaderView.web.d.ts.map +1 -0
  16. package/dist/features/reader/adapters/ReaderView.web.js +16 -0
  17. package/dist/features/reader/adapters/ReaderView.web.js.map +1 -0
  18. package/dist/features/reader/drivers/EpubReaderDriver.d.ts +20 -0
  19. package/dist/features/reader/drivers/EpubReaderDriver.d.ts.map +1 -0
  20. package/dist/features/reader/drivers/EpubReaderDriver.js +259 -0
  21. package/dist/features/reader/drivers/EpubReaderDriver.js.map +1 -0
  22. package/dist/features/reader/drivers/PdfReaderDriver.d.ts +18 -0
  23. package/dist/features/reader/drivers/PdfReaderDriver.d.ts.map +1 -0
  24. package/dist/features/reader/drivers/PdfReaderDriver.js +198 -0
  25. package/dist/features/reader/drivers/PdfReaderDriver.js.map +1 -0
  26. package/dist/features/reader/epub/EpubArchive.d.ts +11 -0
  27. package/dist/features/reader/epub/EpubArchive.d.ts.map +1 -0
  28. package/dist/features/reader/epub/EpubArchive.js +65 -0
  29. package/dist/features/reader/epub/EpubArchive.js.map +1 -0
  30. package/dist/features/reader/epub/EpubArchiveFetcher.d.ts +11 -0
  31. package/dist/features/reader/epub/EpubArchiveFetcher.d.ts.map +1 -0
  32. package/dist/features/reader/epub/EpubArchiveFetcher.js +44 -0
  33. package/dist/features/reader/epub/EpubArchiveFetcher.js.map +1 -0
  34. package/dist/features/reader/epub/mediaTypes.d.ts +3 -0
  35. package/dist/features/reader/epub/mediaTypes.d.ts.map +1 -0
  36. package/dist/features/reader/epub/mediaTypes.js +26 -0
  37. package/dist/features/reader/epub/mediaTypes.js.map +1 -0
  38. package/dist/features/reader/epub/parseEpubPackage.d.ts +22 -0
  39. package/dist/features/reader/epub/parseEpubPackage.d.ts.map +1 -0
  40. package/dist/features/reader/epub/parseEpubPackage.js +98 -0
  41. package/dist/features/reader/epub/parseEpubPackage.js.map +1 -0
  42. package/dist/features/reader/epub/path.d.ts +4 -0
  43. package/dist/features/reader/epub/path.d.ts.map +1 -0
  44. package/dist/features/reader/epub/path.js +28 -0
  45. package/dist/features/reader/epub/path.js.map +1 -0
  46. package/dist/features/reader/epub/sanitizeEpubDocument.d.ts +3 -0
  47. package/dist/features/reader/epub/sanitizeEpubDocument.d.ts.map +1 -0
  48. package/dist/features/reader/epub/sanitizeEpubDocument.js +90 -0
  49. package/dist/features/reader/epub/sanitizeEpubDocument.js.map +1 -0
  50. package/dist/features/reader/epub/validateEpubArchiveEntriesAsync.d.ts +4 -0
  51. package/dist/features/reader/epub/validateEpubArchiveEntriesAsync.d.ts.map +1 -0
  52. package/dist/features/reader/epub/validateEpubArchiveEntriesAsync.js +50 -0
  53. package/dist/features/reader/epub/validateEpubArchiveEntriesAsync.js.map +1 -0
  54. package/dist/features/reader/errors/ReaderDocumentError.d.ts +6 -0
  55. package/dist/features/reader/errors/ReaderDocumentError.d.ts.map +1 -0
  56. package/dist/features/reader/errors/ReaderDocumentError.js +9 -0
  57. package/dist/features/reader/errors/ReaderDocumentError.js.map +1 -0
  58. package/dist/features/reader/loadReaderSourceAsync.d.ts +2 -0
  59. package/dist/features/reader/loadReaderSourceAsync.d.ts.map +1 -0
  60. package/dist/features/reader/loadReaderSourceAsync.js +49 -0
  61. package/dist/features/reader/loadReaderSourceAsync.js.map +1 -0
  62. package/dist/features/reader/navigation/resolveReaderKeyboardIntent.d.ts +9 -0
  63. package/dist/features/reader/navigation/resolveReaderKeyboardIntent.d.ts.map +1 -0
  64. package/dist/features/reader/navigation/resolveReaderKeyboardIntent.js +19 -0
  65. package/dist/features/reader/navigation/resolveReaderKeyboardIntent.js.map +1 -0
  66. package/dist/features/reader/navigation/resolveReaderNavigationIntent.d.ts +11 -0
  67. package/dist/features/reader/navigation/resolveReaderNavigationIntent.d.ts.map +1 -0
  68. package/dist/features/reader/navigation/resolveReaderNavigationIntent.js +16 -0
  69. package/dist/features/reader/navigation/resolveReaderNavigationIntent.js.map +1 -0
  70. package/dist/features/reader/readerViewportPresentation.d.ts +4 -0
  71. package/dist/features/reader/readerViewportPresentation.d.ts.map +1 -0
  72. package/dist/features/reader/readerViewportPresentation.js +25 -0
  73. package/dist/features/reader/readerViewportPresentation.js.map +1 -0
  74. package/dist/features/reader/toReaderViewportProps.d.ts +4 -0
  75. package/dist/features/reader/toReaderViewportProps.d.ts.map +1 -0
  76. package/dist/features/reader/toReaderViewportProps.js +24 -0
  77. package/dist/features/reader/toReaderViewportProps.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +2 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/index.native.d.ts +3 -0
  83. package/dist/index.native.d.ts.map +1 -0
  84. package/dist/index.native.js +2 -0
  85. package/dist/index.native.js.map +1 -0
  86. package/dist/index.web.d.ts +3 -0
  87. package/dist/index.web.d.ts.map +1 -0
  88. package/dist/index.web.js +2 -0
  89. package/dist/index.web.js.map +1 -0
  90. package/dist/types/reader.d.ts +93 -0
  91. package/dist/types/reader.d.ts.map +1 -0
  92. package/dist/types/reader.js +2 -0
  93. package/dist/types/reader.js.map +1 -0
  94. package/package.json +105 -4
  95. package/README.md +0 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EpubArchive.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/EpubArchive.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Entry,\n type FileEntry,\n Uint8ArrayReader,\n Uint8ArrayWriter,\n ZipReader,\n} from '@zip.js/zip.js';\n\nimport { ReaderDocumentError } from '../errors/ReaderDocumentError.js';\nimport { normalizeArchivePath } from './path.js';\nimport { validateEpubArchiveEntriesAsync } from './validateEpubArchiveEntriesAsync.js';\n\nexport class EpubArchive {\n readonly #entries: Map<string, FileEntry>;\n readonly #reader: ZipReader<Uint8Array>;\n\n private constructor(reader: ZipReader<Uint8Array>, entries: readonly Entry[]) {\n this.#reader = reader;\n this.#entries = createEntryMap(entries);\n }\n\n static async open(bytes: Uint8Array, signal: AbortSignal): Promise<EpubArchive> {\n const reader = new ZipReader(new Uint8ArrayReader(bytes), { strictness: 'strict' });\n try {\n const entries = await reader.getEntries({ strictness: 'strict' });\n await validateEpubArchiveEntriesAsync(entries, signal);\n if (entries.some((entry) => !entry.directory && entry.encrypted)) {\n throw new ReaderDocumentError(\n 'protected-document',\n 'This EPUB uses content protection and cannot be opened.',\n );\n }\n return new EpubArchive(reader, entries);\n } catch (error) {\n await reader.close();\n throw error;\n }\n }\n\n paths(): readonly string[] {\n return [...this.#entries.keys()];\n }\n\n has(path: string): boolean {\n return this.#entries.has(normalizeArchivePath(path));\n }\n\n async read(path: string, signal?: AbortSignal): Promise<Uint8Array> {\n const normalizedPath = normalizeArchivePath(path);\n const entry = this.#entries.get(normalizedPath);\n if (entry === undefined) throw new Error(`Missing EPUB resource: ${normalizedPath}`);\n return entry.getData(new Uint8ArrayWriter(), {\n checkCrc32: true,\n checkOverlappingEntry: true,\n ...(signal === undefined ? {} : { signal }),\n strictness: 'strict',\n });\n }\n\n async readText(path: string, signal?: AbortSignal): Promise<string> {\n return new TextDecoder().decode(await this.read(path, signal));\n }\n\n async close(): Promise<void> {\n await this.#reader.close();\n }\n}\n\nfunction createEntryMap(entries: readonly Entry[]): Map<string, FileEntry> {\n const files = new Map<string, FileEntry>();\n for (const entry of entries) {\n if (entry.directory) continue;\n const normalizedPath = normalizeArchivePath(entry.filename);\n if (files.has(normalizedPath)) {\n throw new ReaderDocumentError(\n 'invalid-document',\n `The EPUB contains duplicate resource path \"${normalizedPath}\".`,\n );\n }\n files.set(normalizedPath, entry);\n }\n return files;\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { Link } from '@readium/shared';
2
+ import { type Fetcher, Resource } from '@readium/shared';
3
+ import type { EpubArchive } from './EpubArchive.js';
4
+ export declare class EpubArchiveFetcher implements Fetcher {
5
+ #private;
6
+ constructor(archive: EpubArchive, links: readonly Link[], overrides: ReadonlyMap<string, Uint8Array>);
7
+ links(): Link[];
8
+ get(link: Link): Resource;
9
+ close(): void;
10
+ }
11
+ //# sourceMappingURL=EpubArchiveFetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EpubArchiveFetcher.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/EpubArchiveFetcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,KAAK,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,qBAAa,kBAAmB,YAAW,OAAO;;gBAM9C,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC;IAO5C,KAAK,IAAI,IAAI,EAAE;IAIf,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ;IAIzB,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,44 @@
1
+ import { Resource } from '@readium/shared';
2
+ export class EpubArchiveFetcher {
3
+ #archive;
4
+ #links;
5
+ #overrides;
6
+ constructor(archive, links, overrides) {
7
+ this.#archive = archive;
8
+ this.#links = [...links];
9
+ this.#overrides = overrides;
10
+ }
11
+ links() {
12
+ return [...this.#links];
13
+ }
14
+ get(link) {
15
+ return new EpubArchiveResource(this.#archive, link, this.#overrides.get(link.href));
16
+ }
17
+ close() {
18
+ void this.#archive.close();
19
+ }
20
+ }
21
+ class EpubArchiveResource extends Resource {
22
+ archive;
23
+ resourceLink;
24
+ override;
25
+ constructor(archive, resourceLink, override) {
26
+ super();
27
+ this.archive = archive;
28
+ this.resourceLink = resourceLink;
29
+ this.override = override;
30
+ }
31
+ link() {
32
+ return Promise.resolve(this.resourceLink);
33
+ }
34
+ async length() {
35
+ return (this.override ?? (await this.archive.read(this.resourceLink.href))).byteLength;
36
+ }
37
+ async read() {
38
+ return this.override ?? this.archive.read(this.resourceLink.href);
39
+ }
40
+ close() {
41
+ // Archive lifetime is owned by the driver.
42
+ }
43
+ }
44
+ //# sourceMappingURL=EpubArchiveFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EpubArchiveFetcher.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/EpubArchiveFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIzD,MAAM,OAAO,kBAAkB;IACpB,QAAQ,CAAc;IACtB,MAAM,CAAS;IACf,UAAU,CAAkC;IAErD,YACE,OAAoB,EACpB,KAAsB,EACtB,SAA0C;QAE1C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,KAAK;QACH,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,IAAU;QACZ,OAAO,IAAI,mBAAmB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACtF,CAAC;IAED,KAAK;QACH,KAAK,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF;AAED,MAAM,mBAAoB,SAAQ,QAAQ;IAE7B;IACA;IACA;IAHX,YACW,OAAoB,EACpB,YAAkB,EAClB,QAAgC;QAEzC,KAAK,EAAE,CAAC;QAJC,YAAO,GAAP,OAAO,CAAa;QACpB,iBAAY,GAAZ,YAAY,CAAM;QAClB,aAAQ,GAAR,QAAQ,CAAwB;IAG3C,CAAC;IAED,IAAI;QACF,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC;IAED,KAAK;QACH,2CAA2C;IAC7C,CAAC;CACF","sourcesContent":["import type { Link } from '@readium/shared';\nimport { type Fetcher, Resource } from '@readium/shared';\n\nimport type { EpubArchive } from './EpubArchive.js';\n\nexport class EpubArchiveFetcher implements Fetcher {\n readonly #archive: EpubArchive;\n readonly #links: Link[];\n readonly #overrides: ReadonlyMap<string, Uint8Array>;\n\n constructor(\n archive: EpubArchive,\n links: readonly Link[],\n overrides: ReadonlyMap<string, Uint8Array>,\n ) {\n this.#archive = archive;\n this.#links = [...links];\n this.#overrides = overrides;\n }\n\n links(): Link[] {\n return [...this.#links];\n }\n\n get(link: Link): Resource {\n return new EpubArchiveResource(this.#archive, link, this.#overrides.get(link.href));\n }\n\n close(): void {\n void this.#archive.close();\n }\n}\n\nclass EpubArchiveResource extends Resource {\n constructor(\n readonly archive: EpubArchive,\n readonly resourceLink: Link,\n readonly override: Uint8Array | undefined,\n ) {\n super();\n }\n\n link(): Promise<Link> {\n return Promise.resolve(this.resourceLink);\n }\n\n async length(): Promise<number | undefined> {\n return (this.override ?? (await this.archive.read(this.resourceLink.href))).byteLength;\n }\n\n async read(): Promise<Uint8Array | undefined> {\n return this.override ?? this.archive.read(this.resourceLink.href);\n }\n\n close(): void {\n // Archive lifetime is owned by the driver.\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare const HTML_MEDIA_TYPES: Set<string>;
2
+ export declare function inferEpubMediaType(path: string): string;
3
+ //# sourceMappingURL=mediaTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediaTypes.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/mediaTypes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,aAAkD,CAAC;AAEhF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAavD"}
@@ -0,0 +1,26 @@
1
+ export const HTML_MEDIA_TYPES = new Set(['application/xhtml+xml', 'text/html']);
2
+ export function inferEpubMediaType(path) {
3
+ const extension = path.split('.').pop()?.toLowerCase() ?? '';
4
+ if (extension === 'css')
5
+ return 'text/css';
6
+ if (extension === 'gif')
7
+ return 'image/gif';
8
+ if (extension === 'html')
9
+ return 'text/html';
10
+ if (extension === 'jpeg' || extension === 'jpg')
11
+ return 'image/jpeg';
12
+ if (extension === 'png')
13
+ return 'image/png';
14
+ if (extension === 'svg')
15
+ return 'image/svg+xml';
16
+ if (extension === 'ttf')
17
+ return 'font/ttf';
18
+ if (extension === 'woff')
19
+ return 'font/woff';
20
+ if (extension === 'woff2')
21
+ return 'font/woff2';
22
+ if (extension === 'xhtml')
23
+ return 'application/xhtml+xml';
24
+ return 'application/octet-stream';
25
+ }
26
+ //# sourceMappingURL=mediaTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediaTypes.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/mediaTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,uBAAuB,EAAE,WAAW,CAAC,CAAC,CAAC;AAEhF,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7D,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IAC3C,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IAC5C,IAAI,SAAS,KAAK,MAAM;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,SAAS,KAAK,MAAM,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,YAAY,CAAC;IACrE,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IAC5C,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,eAAe,CAAC;IAChD,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IAC3C,IAAI,SAAS,KAAK,MAAM;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,SAAS,KAAK,OAAO;QAAE,OAAO,YAAY,CAAC;IAC/C,IAAI,SAAS,KAAK,OAAO;QAAE,OAAO,uBAAuB,CAAC;IAC1D,OAAO,0BAA0B,CAAC;AACpC,CAAC","sourcesContent":["export const HTML_MEDIA_TYPES = new Set(['application/xhtml+xml', 'text/html']);\n\nexport function inferEpubMediaType(path: string): string {\n const extension = path.split('.').pop()?.toLowerCase() ?? '';\n if (extension === 'css') return 'text/css';\n if (extension === 'gif') return 'image/gif';\n if (extension === 'html') return 'text/html';\n if (extension === 'jpeg' || extension === 'jpg') return 'image/jpeg';\n if (extension === 'png') return 'image/png';\n if (extension === 'svg') return 'image/svg+xml';\n if (extension === 'ttf') return 'font/ttf';\n if (extension === 'woff') return 'font/woff';\n if (extension === 'woff2') return 'font/woff2';\n if (extension === 'xhtml') return 'application/xhtml+xml';\n return 'application/octet-stream';\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { EpubArchive } from './EpubArchive.js';
2
+ interface EpubPackageItem {
3
+ readonly href: string;
4
+ readonly mediaType: string;
5
+ readonly properties: readonly string[];
6
+ }
7
+ export interface ParsedEpubPackage {
8
+ readonly direction: 'ltr' | 'rtl';
9
+ readonly fixedLayout: boolean;
10
+ readonly language?: string;
11
+ readonly packagePath: string;
12
+ readonly readingOrder: readonly EpubPackageItem[];
13
+ readonly resources: readonly EpubPackageItem[];
14
+ readonly tableOfContents: readonly {
15
+ readonly href: string;
16
+ readonly title: string;
17
+ }[];
18
+ readonly title: string;
19
+ }
20
+ export declare function parseEpubPackage(archive: EpubArchive, signal: AbortSignal): Promise<ParsedEpubPackage>;
21
+ export {};
22
+ //# sourceMappingURL=parseEpubPackage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseEpubPackage.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/parseEpubPackage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,UAAU,eAAe;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,SAAS,eAAe,EAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,QAAQ,CAAC,eAAe,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAmF5B"}
@@ -0,0 +1,98 @@
1
+ /* eslint-disable max-lines-per-function, complexity -- Package parsing validates one transactional document graph. */
2
+ import { ReaderDocumentError } from '../errors/ReaderDocumentError.js';
3
+ import { resolveArchivePath } from './path.js';
4
+ export async function parseEpubPackage(archive, signal) {
5
+ const mimetype = (await archive.readText('mimetype', signal)).trim();
6
+ if (mimetype !== 'application/epub+zip') {
7
+ throw new ReaderDocumentError('invalid-document', 'The archive is not an EPUB document.');
8
+ }
9
+ if (archive.has('META-INF/encryption.xml')) {
10
+ const encryption = await archive.readText('META-INF/encryption.xml', signal);
11
+ if (elementsByLocalName(parseXml(encryption, 'EPUB encryption metadata'), 'EncryptedData')
12
+ .length > 0) {
13
+ throw new ReaderDocumentError('protected-document', 'This EPUB uses content protection and cannot be opened.');
14
+ }
15
+ }
16
+ const container = parseXml(await archive.readText('META-INF/container.xml', signal), 'EPUB container');
17
+ const [rootfile] = elementsByLocalName(container, 'rootfile');
18
+ const packagePathValue = rootfile?.getAttribute('full-path');
19
+ if (!packagePathValue) {
20
+ throw new ReaderDocumentError('invalid-document', 'The EPUB container has no package document.');
21
+ }
22
+ const packagePath = resolveArchivePath('', packagePathValue);
23
+ const packageDocument = parseXml(await archive.readText(packagePath, signal), 'EPUB package');
24
+ const manifestItems = new Map();
25
+ for (const item of elementsByLocalName(packageDocument, 'item')) {
26
+ const id = item.getAttribute('id');
27
+ const href = item.getAttribute('href');
28
+ const mediaType = item.getAttribute('media-type');
29
+ if (!id || !href || !mediaType)
30
+ continue;
31
+ manifestItems.set(id, {
32
+ href: resolveArchivePath(packagePath, href),
33
+ mediaType,
34
+ properties: (item.getAttribute('properties') ?? '').split(/\s+/).filter(Boolean),
35
+ });
36
+ }
37
+ const [spine] = elementsByLocalName(packageDocument, 'spine');
38
+ const readingOrder = elementsByLocalName(spine ?? packageDocument, 'itemref')
39
+ .map((itemref) => manifestItems.get(itemref.getAttribute('idref') ?? ''))
40
+ .filter((item) => item !== undefined);
41
+ if (readingOrder.length === 0) {
42
+ throw new ReaderDocumentError('invalid-document', 'The EPUB package has no readable spine.');
43
+ }
44
+ const metadata = elementsByLocalName(packageDocument, 'metadata')[0] ?? packageDocument.documentElement;
45
+ const title = elementText(metadata, 'title') ?? 'Untitled publication';
46
+ const language = elementText(metadata, 'language');
47
+ const fixedLayout = elementsByLocalName(metadata, 'meta').some((element) => element.getAttribute('property') === 'rendition:layout' &&
48
+ element.textContent.trim() === 'pre-paginated') || readingOrder.every((item) => item.properties.includes('rendition:layout-pre-paginated'));
49
+ const direction = spine?.getAttribute('page-progression-direction') === 'rtl' ? 'rtl' : 'ltr';
50
+ const readingPaths = new Set(readingOrder.map((item) => item.href));
51
+ const navigationItem = [...manifestItems.values()].find((item) => item.properties.includes('nav'));
52
+ const tableOfContents = navigationItem
53
+ ? await parseNavigationDocument(archive, navigationItem.href, signal)
54
+ : [];
55
+ return {
56
+ direction,
57
+ fixedLayout,
58
+ ...(language === undefined ? {} : { language }),
59
+ packagePath,
60
+ readingOrder,
61
+ resources: [...manifestItems.values()].filter((item) => !readingPaths.has(item.href)),
62
+ tableOfContents,
63
+ title,
64
+ };
65
+ }
66
+ async function parseNavigationDocument(archive, navigationPath, signal) {
67
+ const document = parseXml(await archive.readText(navigationPath, signal), 'EPUB navigation');
68
+ const navigation = elementsByLocalName(document, 'nav').find((element) => (element.getAttribute('epub:type') ?? element.getAttribute('type') ?? '')
69
+ .split(/\s+/)
70
+ .includes('toc')) ?? elementsByLocalName(document, 'nav')[0];
71
+ if (!navigation)
72
+ return [];
73
+ return elementsByLocalName(navigation, 'a').flatMap((anchor) => {
74
+ const href = anchor.getAttribute('href');
75
+ const title = anchor.textContent.trim();
76
+ if (!href || !title)
77
+ return [];
78
+ const [resourceReference, fragment] = href.split('#', 2);
79
+ const resourcePath = resolveArchivePath(navigationPath, resourceReference ?? '');
80
+ return [{ href: fragment ? `${resourcePath}#${fragment}` : resourcePath, title }];
81
+ });
82
+ }
83
+ function parseXml(source, label) {
84
+ const document = new DOMParser().parseFromString(source, 'application/xml');
85
+ if (elementsByLocalName(document, 'parsererror').length > 0) {
86
+ throw new ReaderDocumentError('invalid-document', `Invalid ${label}.`);
87
+ }
88
+ return document;
89
+ }
90
+ function elementsByLocalName(root, localName) {
91
+ return Array.from(root.getElementsByTagName('*')).filter((element) => element.localName === localName);
92
+ }
93
+ function elementText(root, localName) {
94
+ const [element] = elementsByLocalName(root, localName);
95
+ const value = element?.textContent.trim();
96
+ return value === '' ? undefined : value;
97
+ }
98
+ //# sourceMappingURL=parseEpubPackage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseEpubPackage.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/parseEpubPackage.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable max-lines-per-function, complexity -- Package parsing validates one transactional document graph. */\nimport { ReaderDocumentError } from '../errors/ReaderDocumentError.js';\nimport type { EpubArchive } from './EpubArchive.js';\nimport { resolveArchivePath } from './path.js';\n\ninterface EpubPackageItem {\n readonly href: string;\n readonly mediaType: string;\n readonly properties: readonly string[];\n}\n\nexport interface ParsedEpubPackage {\n readonly direction: 'ltr' | 'rtl';\n readonly fixedLayout: boolean;\n readonly language?: string;\n readonly packagePath: string;\n readonly readingOrder: readonly EpubPackageItem[];\n readonly resources: readonly EpubPackageItem[];\n readonly tableOfContents: readonly { readonly href: string; readonly title: string }[];\n readonly title: string;\n}\n\nexport async function parseEpubPackage(\n archive: EpubArchive,\n signal: AbortSignal,\n): Promise<ParsedEpubPackage> {\n const mimetype = (await archive.readText('mimetype', signal)).trim();\n if (mimetype !== 'application/epub+zip') {\n throw new ReaderDocumentError('invalid-document', 'The archive is not an EPUB document.');\n }\n if (archive.has('META-INF/encryption.xml')) {\n const encryption = await archive.readText('META-INF/encryption.xml', signal);\n if (\n elementsByLocalName(parseXml(encryption, 'EPUB encryption metadata'), 'EncryptedData')\n .length > 0\n ) {\n throw new ReaderDocumentError(\n 'protected-document',\n 'This EPUB uses content protection and cannot be opened.',\n );\n }\n }\n\n const container = parseXml(\n await archive.readText('META-INF/container.xml', signal),\n 'EPUB container',\n );\n const [rootfile] = elementsByLocalName(container, 'rootfile');\n const packagePathValue = rootfile?.getAttribute('full-path');\n if (!packagePathValue) {\n throw new ReaderDocumentError(\n 'invalid-document',\n 'The EPUB container has no package document.',\n );\n }\n const packagePath = resolveArchivePath('', packagePathValue);\n const packageDocument = parseXml(await archive.readText(packagePath, signal), 'EPUB package');\n\n const manifestItems = new Map<string, EpubPackageItem>();\n for (const item of elementsByLocalName(packageDocument, 'item')) {\n const id = item.getAttribute('id');\n const href = item.getAttribute('href');\n const mediaType = item.getAttribute('media-type');\n if (!id || !href || !mediaType) continue;\n manifestItems.set(id, {\n href: resolveArchivePath(packagePath, href),\n mediaType,\n properties: (item.getAttribute('properties') ?? '').split(/\\s+/).filter(Boolean),\n });\n }\n\n const [spine] = elementsByLocalName(packageDocument, 'spine');\n const readingOrder = elementsByLocalName(spine ?? packageDocument, 'itemref')\n .map((itemref) => manifestItems.get(itemref.getAttribute('idref') ?? ''))\n .filter((item): item is EpubPackageItem => item !== undefined);\n if (readingOrder.length === 0) {\n throw new ReaderDocumentError('invalid-document', 'The EPUB package has no readable spine.');\n }\n\n const metadata =\n elementsByLocalName(packageDocument, 'metadata')[0] ?? packageDocument.documentElement;\n const title = elementText(metadata, 'title') ?? 'Untitled publication';\n const language = elementText(metadata, 'language');\n const fixedLayout =\n elementsByLocalName(metadata, 'meta').some(\n (element) =>\n element.getAttribute('property') === 'rendition:layout' &&\n element.textContent.trim() === 'pre-paginated',\n ) || readingOrder.every((item) => item.properties.includes('rendition:layout-pre-paginated'));\n const direction = spine?.getAttribute('page-progression-direction') === 'rtl' ? 'rtl' : 'ltr';\n const readingPaths = new Set(readingOrder.map((item) => item.href));\n const navigationItem = [...manifestItems.values()].find((item) =>\n item.properties.includes('nav'),\n );\n const tableOfContents = navigationItem\n ? await parseNavigationDocument(archive, navigationItem.href, signal)\n : [];\n\n return {\n direction,\n fixedLayout,\n ...(language === undefined ? {} : { language }),\n packagePath,\n readingOrder,\n resources: [...manifestItems.values()].filter((item) => !readingPaths.has(item.href)),\n tableOfContents,\n title,\n };\n}\n\nasync function parseNavigationDocument(\n archive: EpubArchive,\n navigationPath: string,\n signal: AbortSignal,\n): Promise<{ href: string; title: string }[]> {\n const document = parseXml(await archive.readText(navigationPath, signal), 'EPUB navigation');\n const navigation =\n elementsByLocalName(document, 'nav').find((element) =>\n (element.getAttribute('epub:type') ?? element.getAttribute('type') ?? '')\n .split(/\\s+/)\n .includes('toc'),\n ) ?? elementsByLocalName(document, 'nav')[0];\n if (!navigation) return [];\n return elementsByLocalName(navigation, 'a').flatMap((anchor) => {\n const href = anchor.getAttribute('href');\n const title = anchor.textContent.trim();\n if (!href || !title) return [];\n const [resourceReference, fragment] = href.split('#', 2);\n const resourcePath = resolveArchivePath(navigationPath, resourceReference ?? '');\n return [{ href: fragment ? `${resourcePath}#${fragment}` : resourcePath, title }];\n });\n}\n\nfunction parseXml(source: string, label: string): XMLDocument {\n const document = new DOMParser().parseFromString(source, 'application/xml');\n if (elementsByLocalName(document, 'parsererror').length > 0) {\n throw new ReaderDocumentError('invalid-document', `Invalid ${label}.`);\n }\n return document;\n}\n\nfunction elementsByLocalName(root: Document | Element, localName: string): Element[] {\n return Array.from(root.getElementsByTagName('*')).filter(\n (element) => element.localName === localName,\n );\n}\n\nfunction elementText(root: Document | Element, localName: string): string | undefined {\n const [element] = elementsByLocalName(root, localName);\n const value = element?.textContent.trim();\n return value === '' ? undefined : value;\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export declare function normalizeArchivePath(path: string): string;
2
+ export declare function resolveArchivePath(basePath: string, reference: string): string;
3
+ export declare function isExternalReference(reference: string): boolean;
4
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/path.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAO9E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE9D"}
@@ -0,0 +1,28 @@
1
+ export function normalizeArchivePath(path) {
2
+ const segments = [];
3
+ for (const segment of decodeURIComponent(path).replaceAll('\\', '/').split('/')) {
4
+ if (segment === '' || segment === '.')
5
+ continue;
6
+ if (segment === '..') {
7
+ if (segments.length === 0)
8
+ throw new Error(`EPUB resource escapes the archive: ${path}`);
9
+ segments.pop();
10
+ continue;
11
+ }
12
+ segments.push(segment);
13
+ }
14
+ return segments.join('/');
15
+ }
16
+ export function resolveArchivePath(basePath, reference) {
17
+ const cleanReference = reference.split('#', 1)[0]?.split('?', 1)[0] ?? '';
18
+ if (cleanReference.startsWith('/'))
19
+ return normalizeArchivePath(cleanReference);
20
+ const baseDirectory = basePath.includes('/')
21
+ ? basePath.slice(0, basePath.lastIndexOf('/') + 1)
22
+ : '';
23
+ return normalizeArchivePath(`${baseDirectory}${cleanReference}`);
24
+ }
25
+ export function isExternalReference(reference) {
26
+ return /^(?:https?:|mailto:|tel:|data:|javascript:|\/\/)/i.test(reference.trim());
27
+ }
28
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/path.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,OAAO,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAChF,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,GAAG;YAAE,SAAS;QAChD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,EAAE,CAAC,CAAC;YACzF,QAAQ,CAAC,GAAG,EAAE,CAAC;YACf,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,SAAiB;IACpE,MAAM,cAAc,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1E,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAC,cAAc,CAAC,CAAC;IAChF,MAAM,aAAa,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC1C,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClD,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,oBAAoB,CAAC,GAAG,aAAa,GAAG,cAAc,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,OAAO,mDAAmD,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;AACpF,CAAC","sourcesContent":["export function normalizeArchivePath(path: string): string {\n const segments: string[] = [];\n for (const segment of decodeURIComponent(path).replaceAll('\\\\', '/').split('/')) {\n if (segment === '' || segment === '.') continue;\n if (segment === '..') {\n if (segments.length === 0) throw new Error(`EPUB resource escapes the archive: ${path}`);\n segments.pop();\n continue;\n }\n segments.push(segment);\n }\n return segments.join('/');\n}\n\nexport function resolveArchivePath(basePath: string, reference: string): string {\n const cleanReference = reference.split('#', 1)[0]?.split('?', 1)[0] ?? '';\n if (cleanReference.startsWith('/')) return normalizeArchivePath(cleanReference);\n const baseDirectory = basePath.includes('/')\n ? basePath.slice(0, basePath.lastIndexOf('/') + 1)\n : '';\n return normalizeArchivePath(`${baseDirectory}${cleanReference}`);\n}\n\nexport function isExternalReference(reference: string): boolean {\n return /^(?:https?:|mailto:|tel:|data:|javascript:|\\/\\/)/i.test(reference.trim());\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function sanitizeEpubDocument(source: string, resourcePath: string, resolveResourceUrl: (path: string) => string | undefined): string;
2
+ export declare function sanitizeEpubStylesheet(source: string, resourcePath: string, resolveResourceUrl: (path: string) => string | undefined): string;
3
+ //# sourceMappingURL=sanitizeEpubDocument.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizeEpubDocument.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/sanitizeEpubDocument.ts"],"names":[],"mappings":"AAKA,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACvD,MAAM,CAkBR;AAuDD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACvD,MAAM,CAaR"}
@@ -0,0 +1,90 @@
1
+ import { isExternalReference, resolveArchivePath } from './path.js';
2
+ const REMOVED_ELEMENTS = 'script, form, iframe, frame, object, embed';
3
+ const RESOURCE_ATTRIBUTES = ['src', 'poster', 'data'];
4
+ export function sanitizeEpubDocument(source, resourcePath, resolveResourceUrl) {
5
+ const document = new DOMParser().parseFromString(source, 'application/xhtml+xml');
6
+ if (document.querySelector('parsererror')) {
7
+ throw new Error(`Invalid EPUB XHTML resource: ${resourcePath}`);
8
+ }
9
+ document.querySelectorAll(REMOVED_ELEMENTS).forEach((element) => element.remove());
10
+ document
11
+ .querySelectorAll('meta[http-equiv]')
12
+ .forEach((element) => element.getAttribute('http-equiv')?.toLowerCase() === 'refresh' && element.remove());
13
+ sanitizeElementAttributes(document, resourcePath, resolveResourceUrl);
14
+ rewriteDocumentResources(document, resourcePath, resolveResourceUrl);
15
+ rewriteDocumentLinks(document);
16
+ return new XMLSerializer().serializeToString(document);
17
+ }
18
+ function sanitizeElementAttributes(document, resourcePath, resolveResourceUrl) {
19
+ for (const element of document.querySelectorAll('*')) {
20
+ for (const attribute of Array.from(element.attributes)) {
21
+ if (attribute.name.toLowerCase().startsWith('on'))
22
+ element.removeAttribute(attribute.name);
23
+ }
24
+ const inlineStyle = element.getAttribute('style');
25
+ if (inlineStyle) {
26
+ element.setAttribute('style', sanitizeEpubStylesheet(inlineStyle, resourcePath, resolveResourceUrl));
27
+ }
28
+ element.removeAttribute('srcset');
29
+ }
30
+ for (const style of document.querySelectorAll('style')) {
31
+ style.textContent = sanitizeEpubStylesheet(style.textContent, resourcePath, resolveResourceUrl);
32
+ }
33
+ }
34
+ function rewriteDocumentResources(document, resourcePath, resolveResourceUrl) {
35
+ for (const attributeName of RESOURCE_ATTRIBUTES) {
36
+ for (const element of document.querySelectorAll(`[${attributeName}]`)) {
37
+ rewriteResourceAttribute(element, attributeName, resourcePath, resolveResourceUrl);
38
+ }
39
+ }
40
+ for (const link of document.querySelectorAll('link[href]')) {
41
+ rewriteResourceAttribute(link, 'href', resourcePath, resolveResourceUrl);
42
+ }
43
+ }
44
+ function rewriteDocumentLinks(document) {
45
+ for (const anchor of document.querySelectorAll('a[href]')) {
46
+ const href = anchor.getAttribute('href')?.trim();
47
+ if (!href)
48
+ continue;
49
+ if (/^(?:https?:|mailto:|tel:|\/\/)/i.test(href)) {
50
+ anchor.setAttribute('data-reader-external-href', href);
51
+ anchor.removeAttribute('href');
52
+ }
53
+ else if (isExternalReference(href)) {
54
+ anchor.removeAttribute('href');
55
+ }
56
+ }
57
+ }
58
+ export function sanitizeEpubStylesheet(source, resourcePath, resolveResourceUrl) {
59
+ const withoutImports = source.replaceAll(/@import\s+(?:url\([^)]*\)|[^;]+);?/gi, '');
60
+ return withoutImports.replaceAll(/url\(\s*(['"]?)([^)'"\s]+)\1\s*\)/gi, (match, _quote, value) => {
61
+ if (isExternalReference(value))
62
+ return 'url(about:blank)';
63
+ try {
64
+ return `url("${resolveResourceUrl(resolveArchivePath(resourcePath, value)) ?? 'about:blank'}")`;
65
+ }
66
+ catch {
67
+ return 'url(about:blank)';
68
+ }
69
+ });
70
+ }
71
+ function rewriteResourceAttribute(element, attributeName, resourcePath, resolveResourceUrl) {
72
+ const value = element.getAttribute(attributeName)?.trim();
73
+ if (!value)
74
+ return;
75
+ if (isExternalReference(value)) {
76
+ element.removeAttribute(attributeName);
77
+ return;
78
+ }
79
+ try {
80
+ const resolved = resolveResourceUrl(resolveArchivePath(resourcePath, value));
81
+ if (resolved === undefined)
82
+ element.removeAttribute(attributeName);
83
+ else
84
+ element.setAttribute(attributeName, resolved);
85
+ }
86
+ catch {
87
+ element.removeAttribute(attributeName);
88
+ }
89
+ }
90
+ //# sourceMappingURL=sanitizeEpubDocument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizeEpubDocument.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/sanitizeEpubDocument.ts"],"names":[],"mappings":"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","sourcesContent":["import { isExternalReference, resolveArchivePath } from './path.js';\n\nconst REMOVED_ELEMENTS = 'script, form, iframe, frame, object, embed';\nconst RESOURCE_ATTRIBUTES = ['src', 'poster', 'data'] as const;\n\nexport function sanitizeEpubDocument(\n source: string,\n resourcePath: string,\n resolveResourceUrl: (path: string) => string | undefined,\n): string {\n const document = new DOMParser().parseFromString(source, 'application/xhtml+xml');\n if (document.querySelector('parsererror')) {\n throw new Error(`Invalid EPUB XHTML resource: ${resourcePath}`);\n }\n\n document.querySelectorAll(REMOVED_ELEMENTS).forEach((element) => element.remove());\n document\n .querySelectorAll('meta[http-equiv]')\n .forEach(\n (element) =>\n element.getAttribute('http-equiv')?.toLowerCase() === 'refresh' && element.remove(),\n );\n\n sanitizeElementAttributes(document, resourcePath, resolveResourceUrl);\n rewriteDocumentResources(document, resourcePath, resolveResourceUrl);\n rewriteDocumentLinks(document);\n return new XMLSerializer().serializeToString(document);\n}\n\nfunction sanitizeElementAttributes(\n document: XMLDocument,\n resourcePath: string,\n resolveResourceUrl: (path: string) => string | undefined,\n): void {\n for (const element of document.querySelectorAll('*')) {\n for (const attribute of Array.from(element.attributes)) {\n if (attribute.name.toLowerCase().startsWith('on')) element.removeAttribute(attribute.name);\n }\n const inlineStyle = element.getAttribute('style');\n if (inlineStyle) {\n element.setAttribute(\n 'style',\n sanitizeEpubStylesheet(inlineStyle, resourcePath, resolveResourceUrl),\n );\n }\n element.removeAttribute('srcset');\n }\n\n for (const style of document.querySelectorAll('style')) {\n style.textContent = sanitizeEpubStylesheet(style.textContent, resourcePath, resolveResourceUrl);\n }\n}\n\nfunction rewriteDocumentResources(\n document: XMLDocument,\n resourcePath: string,\n resolveResourceUrl: (path: string) => string | undefined,\n): void {\n for (const attributeName of RESOURCE_ATTRIBUTES) {\n for (const element of document.querySelectorAll(`[${attributeName}]`)) {\n rewriteResourceAttribute(element, attributeName, resourcePath, resolveResourceUrl);\n }\n }\n\n for (const link of document.querySelectorAll('link[href]')) {\n rewriteResourceAttribute(link, 'href', resourcePath, resolveResourceUrl);\n }\n}\n\nfunction rewriteDocumentLinks(document: XMLDocument): void {\n for (const anchor of document.querySelectorAll('a[href]')) {\n const href = anchor.getAttribute('href')?.trim();\n if (!href) continue;\n if (/^(?:https?:|mailto:|tel:|\\/\\/)/i.test(href)) {\n anchor.setAttribute('data-reader-external-href', href);\n anchor.removeAttribute('href');\n } else if (isExternalReference(href)) {\n anchor.removeAttribute('href');\n }\n }\n}\n\nexport function sanitizeEpubStylesheet(\n source: string,\n resourcePath: string,\n resolveResourceUrl: (path: string) => string | undefined,\n): string {\n const withoutImports = source.replaceAll(/@import\\s+(?:url\\([^)]*\\)|[^;]+);?/gi, '');\n return withoutImports.replaceAll(\n /url\\(\\s*(['\"]?)([^)'\"\\s]+)\\1\\s*\\)/gi,\n (match, _quote, value: string) => {\n if (isExternalReference(value)) return 'url(about:blank)';\n try {\n return `url(\"${resolveResourceUrl(resolveArchivePath(resourcePath, value)) ?? 'about:blank'}\")`;\n } catch {\n return 'url(about:blank)';\n }\n },\n );\n}\n\nfunction rewriteResourceAttribute(\n element: Element,\n attributeName: string,\n resourcePath: string,\n resolveResourceUrl: (path: string) => string | undefined,\n): void {\n const value = element.getAttribute(attributeName)?.trim();\n if (!value) return;\n if (isExternalReference(value)) {\n element.removeAttribute(attributeName);\n return;\n }\n try {\n const resolved = resolveResourceUrl(resolveArchivePath(resourcePath, value));\n if (resolved === undefined) element.removeAttribute(attributeName);\n else element.setAttribute(attributeName, resolved);\n } catch {\n element.removeAttribute(attributeName);\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { Entry } from '@zip.js/zip.js';
2
+ /*** Validate ZIP entry budgets and structure before EPUB extraction. */
3
+ export declare function validateEpubArchiveEntriesAsync(entries: readonly Entry[], signal?: AbortSignal): Promise<void>;
4
+ //# sourceMappingURL=validateEpubArchiveEntriesAsync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateEpubArchiveEntriesAsync.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/epub/validateEpubArchiveEntriesAsync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAQ5C,wEAAwE;AACxE,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -0,0 +1,50 @@
1
+ import { Uint8ArrayWriter } from '@zip.js/zip.js';
2
+ const MAX_COMPRESSION_RATIO = 100;
3
+ const MAX_ENTRY_COUNT = 10_000;
4
+ const MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
5
+ const MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
6
+ /*** Validate ZIP entry budgets and structure before EPUB extraction. */
7
+ export async function validateEpubArchiveEntriesAsync(entries, signal) {
8
+ if (entries.length > MAX_ENTRY_COUNT) {
9
+ throw new Error(`EPUB archive exceeds the ${MAX_ENTRY_COUNT}-entry limit.`);
10
+ }
11
+ let totalUncompressedBytes = 0;
12
+ for (const entry of entries) {
13
+ validateDeclaredSizes(entry);
14
+ if (entry.directory) {
15
+ continue;
16
+ }
17
+ if (entry.uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) {
18
+ throw new Error(`EPUB archive entry "${entry.filename}" exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-byte uncompressed limit.`);
19
+ }
20
+ if (entry.uncompressedSize > 0 &&
21
+ (entry.compressedSize === 0 ||
22
+ entry.uncompressedSize > entry.compressedSize * MAX_COMPRESSION_RATIO)) {
23
+ throw new Error(`EPUB archive entry "${entry.filename}" exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit.`);
24
+ }
25
+ if (totalUncompressedBytes > MAX_TOTAL_UNCOMPRESSED_BYTES - entry.uncompressedSize) {
26
+ throw new Error(`EPUB archive exceeds the ${MAX_TOTAL_UNCOMPRESSED_BYTES}-byte aggregate uncompressed limit.`);
27
+ }
28
+ totalUncompressedBytes += entry.uncompressedSize;
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.directory) {
32
+ continue;
33
+ }
34
+ await entry.getData(new Uint8ArrayWriter(), {
35
+ checkOverlappingEntryOnly: true,
36
+ ...(signal === undefined ? {} : { signal }),
37
+ strictness: 'strict',
38
+ });
39
+ }
40
+ }
41
+ /*** Reject malformed ZIP size metadata before arithmetic or extraction. */
42
+ function validateDeclaredSizes(entry) {
43
+ if (!Number.isSafeInteger(entry.compressedSize) ||
44
+ entry.compressedSize < 0 ||
45
+ !Number.isSafeInteger(entry.uncompressedSize) ||
46
+ entry.uncompressedSize < 0) {
47
+ throw new Error(`EPUB archive entry "${entry.filename}" declares invalid sizes.`);
48
+ }
49
+ }
50
+ //# sourceMappingURL=validateEpubArchiveEntriesAsync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateEpubArchiveEntriesAsync.js","sourceRoot":"","sources":["../../../../src/features/reader/epub/validateEpubArchiveEntriesAsync.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,eAAe,GAAG,MAAM,CAAC;AAC/B,MAAM,4BAA4B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACtD,MAAM,4BAA4B,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvD,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACnD,OAAyB,EACzB,MAAoB;IAEpB,IAAI,OAAO,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,4BAA4B,eAAe,eAAe,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,sBAAsB,GAAG,CAAC,CAAC;IAE/B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAE7B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,gBAAgB,GAAG,4BAA4B,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,uBAAuB,KAAK,CAAC,QAAQ,iBAAiB,4BAA4B,2BAA2B,CAC9G,CAAC;QACJ,CAAC;QAED,IACE,KAAK,CAAC,gBAAgB,GAAG,CAAC;YAC1B,CAAC,KAAK,CAAC,cAAc,KAAK,CAAC;gBACzB,KAAK,CAAC,gBAAgB,GAAG,KAAK,CAAC,cAAc,GAAG,qBAAqB,CAAC,EACxE,CAAC;YACD,MAAM,IAAI,KAAK,CACb,uBAAuB,KAAK,CAAC,QAAQ,iBAAiB,qBAAqB,6BAA6B,CACzG,CAAC;QACJ,CAAC;QAED,IAAI,sBAAsB,GAAG,4BAA4B,GAAG,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACnF,MAAM,IAAI,KAAK,CACb,4BAA4B,4BAA4B,qCAAqC,CAC9F,CAAC;QACJ,CAAC;QAED,sBAAsB,IAAI,KAAK,CAAC,gBAAgB,CAAC;IACnD,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,gBAAgB,EAAE,EAAE;YAC1C,yBAAyB,EAAE,IAAI;YAC/B,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;YAC3C,UAAU,EAAE,QAAQ;SACrB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,SAAS,qBAAqB,CAAC,KAAY;IACzC,IACE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,cAAc,CAAC;QAC3C,KAAK,CAAC,cAAc,GAAG,CAAC;QACxB,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,gBAAgB,CAAC;QAC7C,KAAK,CAAC,gBAAgB,GAAG,CAAC,EAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,CAAC,QAAQ,2BAA2B,CAAC,CAAC;IACpF,CAAC;AACH,CAAC","sourcesContent":["import type { Entry } from '@zip.js/zip.js';\nimport { Uint8ArrayWriter } from '@zip.js/zip.js';\n\nconst MAX_COMPRESSION_RATIO = 100;\nconst MAX_ENTRY_COUNT = 10_000;\nconst MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;\nconst MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;\n\n/*** Validate ZIP entry budgets and structure before EPUB extraction. */\nexport async function validateEpubArchiveEntriesAsync(\n entries: readonly Entry[],\n signal?: AbortSignal,\n): Promise<void> {\n if (entries.length > MAX_ENTRY_COUNT) {\n throw new Error(`EPUB archive exceeds the ${MAX_ENTRY_COUNT}-entry limit.`);\n }\n\n let totalUncompressedBytes = 0;\n\n for (const entry of entries) {\n validateDeclaredSizes(entry);\n\n if (entry.directory) {\n continue;\n }\n\n if (entry.uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) {\n throw new Error(\n `EPUB archive entry \"${entry.filename}\" exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-byte uncompressed limit.`,\n );\n }\n\n if (\n entry.uncompressedSize > 0 &&\n (entry.compressedSize === 0 ||\n entry.uncompressedSize > entry.compressedSize * MAX_COMPRESSION_RATIO)\n ) {\n throw new Error(\n `EPUB archive entry \"${entry.filename}\" exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit.`,\n );\n }\n\n if (totalUncompressedBytes > MAX_TOTAL_UNCOMPRESSED_BYTES - entry.uncompressedSize) {\n throw new Error(\n `EPUB archive exceeds the ${MAX_TOTAL_UNCOMPRESSED_BYTES}-byte aggregate uncompressed limit.`,\n );\n }\n\n totalUncompressedBytes += entry.uncompressedSize;\n }\n\n for (const entry of entries) {\n if (entry.directory) {\n continue;\n }\n\n await entry.getData(new Uint8ArrayWriter(), {\n checkOverlappingEntryOnly: true,\n ...(signal === undefined ? {} : { signal }),\n strictness: 'strict',\n });\n }\n}\n\n/*** Reject malformed ZIP size metadata before arithmetic or extraction. */\nfunction validateDeclaredSizes(entry: Entry): void {\n if (\n !Number.isSafeInteger(entry.compressedSize) ||\n entry.compressedSize < 0 ||\n !Number.isSafeInteger(entry.uncompressedSize) ||\n entry.uncompressedSize < 0\n ) {\n throw new Error(`EPUB archive entry \"${entry.filename}\" declares invalid sizes.`);\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { ReaderErrorCode } from '../../../types/reader.js';
2
+ export declare class ReaderDocumentError extends Error {
3
+ readonly code: ReaderErrorCode;
4
+ constructor(code: ReaderErrorCode, message: string);
5
+ }
6
+ //# sourceMappingURL=ReaderDocumentError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReaderDocumentError.d.ts","sourceRoot":"","sources":["../../../../src/features/reader/errors/ReaderDocumentError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,IAAI,EAAE,eAAe;gBAArB,IAAI,EAAE,eAAe,EAC9B,OAAO,EAAE,MAAM;CAKlB"}
@@ -0,0 +1,9 @@
1
+ export class ReaderDocumentError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'ReaderDocumentError';
7
+ }
8
+ }
9
+ //# sourceMappingURL=ReaderDocumentError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReaderDocumentError.js","sourceRoot":"","sources":["../../../../src/features/reader/errors/ReaderDocumentError.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAEjC;IADX,YACW,IAAqB,EAC9B,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,SAAI,GAAJ,IAAI,CAAiB;QAI9B,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF","sourcesContent":["import type { ReaderErrorCode } from '../../../types/reader.js';\n\nexport class ReaderDocumentError extends Error {\n constructor(\n readonly code: ReaderErrorCode,\n message: string,\n ) {\n super(message);\n this.name = 'ReaderDocumentError';\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function loadReaderSourceAsync(sourceUri: string, signal: AbortSignal): Promise<Uint8Array>;
2
+ //# sourceMappingURL=loadReaderSourceAsync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadReaderSourceAsync.d.ts","sourceRoot":"","sources":["../../../src/features/reader/loadReaderSourceAsync.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,UAAU,CAAC,CA8CrB"}
@@ -0,0 +1,49 @@
1
+ const MAX_READER_SOURCE_BYTES = 256 * 1024 * 1024;
2
+ export async function loadReaderSourceAsync(sourceUri, signal) {
3
+ const response = await fetch(sourceUri, {
4
+ credentials: 'omit',
5
+ signal,
6
+ });
7
+ if (!response.ok) {
8
+ throw new Error(`Reader source request failed with HTTP ${response.status}.`);
9
+ }
10
+ const declaredLength = Number(response.headers.get('content-length'));
11
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_READER_SOURCE_BYTES) {
12
+ throw new Error(`Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`);
13
+ }
14
+ if (response.body === null) {
15
+ return validateSourceSize(new Uint8Array(await response.arrayBuffer()));
16
+ }
17
+ const reader = response.body.getReader();
18
+ const chunks = [];
19
+ let byteLength = 0;
20
+ try {
21
+ for (;;) {
22
+ const result = await reader.read();
23
+ if (result.done)
24
+ break;
25
+ byteLength += result.value.byteLength;
26
+ if (byteLength > MAX_READER_SOURCE_BYTES) {
27
+ throw new Error(`Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`);
28
+ }
29
+ chunks.push(result.value);
30
+ }
31
+ }
32
+ finally {
33
+ reader.releaseLock();
34
+ }
35
+ const bytes = new Uint8Array(byteLength);
36
+ let offset = 0;
37
+ for (const chunk of chunks) {
38
+ bytes.set(chunk, offset);
39
+ offset += chunk.byteLength;
40
+ }
41
+ return bytes;
42
+ }
43
+ function validateSourceSize(bytes) {
44
+ if (bytes.byteLength > MAX_READER_SOURCE_BYTES) {
45
+ throw new Error(`Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`);
46
+ }
47
+ return bytes;
48
+ }
49
+ //# sourceMappingURL=loadReaderSourceAsync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadReaderSourceAsync.js","sourceRoot":"","sources":["../../../src/features/reader/loadReaderSourceAsync.ts"],"names":[],"mappings":"AAAA,MAAM,uBAAuB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,SAAiB,EACjB,MAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACtC,WAAW,EAAE,MAAM;QACnB,MAAM;KACP,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,0CAA0C,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACtE,IAAI,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,cAAc,GAAG,uBAAuB,EAAE,CAAC;QAChF,MAAM,IAAI,KAAK,CAAC,6BAA6B,uBAAuB,uBAAuB,CAAC,CAAC;IAC/F,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO,kBAAkB,CAAC,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM;YACvB,UAAU,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;YACtC,IAAI,UAAU,GAAG,uBAAuB,EAAE,CAAC;gBACzC,MAAM,IAAI,KAAK,CACb,6BAA6B,uBAAuB,uBAAuB,CAC5E,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;IACzC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAiB;IAC3C,IAAI,KAAK,CAAC,UAAU,GAAG,uBAAuB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,6BAA6B,uBAAuB,uBAAuB,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["const MAX_READER_SOURCE_BYTES = 256 * 1024 * 1024;\n\nexport async function loadReaderSourceAsync(\n sourceUri: string,\n signal: AbortSignal,\n): Promise<Uint8Array> {\n const response = await fetch(sourceUri, {\n credentials: 'omit',\n signal,\n });\n\n if (!response.ok) {\n throw new Error(`Reader source request failed with HTTP ${response.status}.`);\n }\n\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > MAX_READER_SOURCE_BYTES) {\n throw new Error(`Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`);\n }\n\n if (response.body === null) {\n return validateSourceSize(new Uint8Array(await response.arrayBuffer()));\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let byteLength = 0;\n\n try {\n for (;;) {\n const result = await reader.read();\n if (result.done) break;\n byteLength += result.value.byteLength;\n if (byteLength > MAX_READER_SOURCE_BYTES) {\n throw new Error(\n `Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`,\n );\n }\n chunks.push(result.value);\n }\n } finally {\n reader.releaseLock();\n }\n\n const bytes = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return bytes;\n}\n\nfunction validateSourceSize(bytes: Uint8Array): Uint8Array {\n if (bytes.byteLength > MAX_READER_SOURCE_BYTES) {\n throw new Error(`Reader source exceeds the ${MAX_READER_SOURCE_BYTES}-byte download limit.`);\n }\n return bytes;\n}\n"]}