wolfy-reader 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 (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -0
  3. package/dist/core/book.d.ts +75 -0
  4. package/dist/core/book.d.ts.map +1 -0
  5. package/dist/core/book.js +2 -0
  6. package/dist/core/book.js.map +1 -0
  7. package/dist/core/errors.d.ts +17 -0
  8. package/dist/core/errors.d.ts.map +1 -0
  9. package/dist/core/errors.js +29 -0
  10. package/dist/core/errors.js.map +1 -0
  11. package/dist/core/format.d.ts +13 -0
  12. package/dist/core/format.d.ts.map +1 -0
  13. package/dist/core/format.js +2 -0
  14. package/dist/core/format.js.map +1 -0
  15. package/dist/core/graphemes.d.ts +36 -0
  16. package/dist/core/graphemes.d.ts.map +1 -0
  17. package/dist/core/graphemes.js +62 -0
  18. package/dist/core/graphemes.js.map +1 -0
  19. package/dist/core/index.d.ts +11 -0
  20. package/dist/core/index.d.ts.map +1 -0
  21. package/dist/core/index.js +6 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/core/lookup.d.ts +10 -0
  24. package/dist/core/lookup.d.ts.map +1 -0
  25. package/dist/core/lookup.js +16 -0
  26. package/dist/core/lookup.js.map +1 -0
  27. package/dist/core/open.d.ts +12 -0
  28. package/dist/core/open.d.ts.map +1 -0
  29. package/dist/core/open.js +23 -0
  30. package/dist/core/open.js.map +1 -0
  31. package/dist/core/position.d.ts +121 -0
  32. package/dist/core/position.d.ts.map +1 -0
  33. package/dist/core/position.js +327 -0
  34. package/dist/core/position.js.map +1 -0
  35. package/dist/core/reading-text.d.ts +77 -0
  36. package/dist/core/reading-text.d.ts.map +1 -0
  37. package/dist/core/reading-text.js +144 -0
  38. package/dist/core/reading-text.js.map +1 -0
  39. package/dist/core/source.d.ts +17 -0
  40. package/dist/core/source.d.ts.map +1 -0
  41. package/dist/core/source.js +39 -0
  42. package/dist/core/source.js.map +1 -0
  43. package/dist/core/storage.d.ts +7 -0
  44. package/dist/core/storage.d.ts.map +1 -0
  45. package/dist/core/storage.js +2 -0
  46. package/dist/core/storage.js.map +1 -0
  47. package/dist/core/text.d.ts +18 -0
  48. package/dist/core/text.d.ts.map +1 -0
  49. package/dist/core/text.js +32 -0
  50. package/dist/core/text.js.map +1 -0
  51. package/dist/formats/epub/href.d.ts +14 -0
  52. package/dist/formats/epub/href.d.ts.map +1 -0
  53. package/dist/formats/epub/href.js +52 -0
  54. package/dist/formats/epub/href.js.map +1 -0
  55. package/dist/formats/epub/index.d.ts +3 -0
  56. package/dist/formats/epub/index.d.ts.map +1 -0
  57. package/dist/formats/epub/index.js +223 -0
  58. package/dist/formats/epub/index.js.map +1 -0
  59. package/dist/formats/epub/opf.d.ts +27 -0
  60. package/dist/formats/epub/opf.d.ts.map +1 -0
  61. package/dist/formats/epub/opf.js +110 -0
  62. package/dist/formats/epub/opf.js.map +1 -0
  63. package/dist/formats/epub/toc.d.ts +6 -0
  64. package/dist/formats/epub/toc.d.ts.map +1 -0
  65. package/dist/formats/epub/toc.js +88 -0
  66. package/dist/formats/epub/toc.js.map +1 -0
  67. package/dist/formats/fb2/index.d.ts +13 -0
  68. package/dist/formats/fb2/index.d.ts.map +1 -0
  69. package/dist/formats/fb2/index.js +317 -0
  70. package/dist/formats/fb2/index.js.map +1 -0
  71. package/dist/formats/index.d.ts +4 -0
  72. package/dist/formats/index.d.ts.map +1 -0
  73. package/dist/formats/index.js +4 -0
  74. package/dist/formats/index.js.map +1 -0
  75. package/dist/formats/text/index.d.ts +11 -0
  76. package/dist/formats/text/index.d.ts.map +1 -0
  77. package/dist/formats/text/index.js +201 -0
  78. package/dist/formats/text/index.js.map +1 -0
  79. package/dist/formats/xml.d.ts +33 -0
  80. package/dist/formats/xml.d.ts.map +1 -0
  81. package/dist/formats/xml.js +321 -0
  82. package/dist/formats/xml.js.map +1 -0
  83. package/dist/index.d.ts +27 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +28 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/layout/chunk.d.ts +51 -0
  88. package/dist/layout/chunk.d.ts.map +1 -0
  89. package/dist/layout/chunk.js +131 -0
  90. package/dist/layout/chunk.js.map +1 -0
  91. package/dist/layout/index.d.ts +238 -0
  92. package/dist/layout/index.d.ts.map +1 -0
  93. package/dist/layout/index.js +476 -0
  94. package/dist/layout/index.js.map +1 -0
  95. package/dist/reader/index.d.ts +231 -0
  96. package/dist/reader/index.d.ts.map +1 -0
  97. package/dist/reader/index.js +958 -0
  98. package/dist/reader/index.js.map +1 -0
  99. package/dist/reader/input.d.ts +69 -0
  100. package/dist/reader/input.d.ts.map +1 -0
  101. package/dist/reader/input.js +75 -0
  102. package/dist/reader/input.js.map +1 -0
  103. package/dist/search/extract.d.ts +51 -0
  104. package/dist/search/extract.d.ts.map +1 -0
  105. package/dist/search/extract.js +401 -0
  106. package/dist/search/extract.js.map +1 -0
  107. package/dist/search/index.d.ts +11 -0
  108. package/dist/search/index.d.ts.map +1 -0
  109. package/dist/search/index.js +11 -0
  110. package/dist/search/index.js.map +1 -0
  111. package/dist/search/matcher.d.ts +56 -0
  112. package/dist/search/matcher.d.ts.map +1 -0
  113. package/dist/search/matcher.js +128 -0
  114. package/dist/search/matcher.js.map +1 -0
  115. package/dist/search/normalize.d.ts +48 -0
  116. package/dist/search/normalize.d.ts.map +1 -0
  117. package/dist/search/normalize.js +103 -0
  118. package/dist/search/normalize.js.map +1 -0
  119. package/dist/view/allowlist.d.ts +59 -0
  120. package/dist/view/allowlist.d.ts.map +1 -0
  121. package/dist/view/allowlist.js +198 -0
  122. package/dist/view/allowlist.js.map +1 -0
  123. package/dist/view/appearance.d.ts +137 -0
  124. package/dist/view/appearance.d.ts.map +1 -0
  125. package/dist/view/appearance.js +274 -0
  126. package/dist/view/appearance.js.map +1 -0
  127. package/dist/view/css.d.ts +24 -0
  128. package/dist/view/css.d.ts.map +1 -0
  129. package/dist/view/css.js +153 -0
  130. package/dist/view/css.js.map +1 -0
  131. package/dist/view/frame.d.ts +54 -0
  132. package/dist/view/frame.d.ts.map +1 -0
  133. package/dist/view/frame.js +980 -0
  134. package/dist/view/frame.js.map +1 -0
  135. package/dist/view/host.d.ts +160 -0
  136. package/dist/view/host.d.ts.map +1 -0
  137. package/dist/view/host.js +385 -0
  138. package/dist/view/host.js.map +1 -0
  139. package/dist/view/index.d.ts +12 -0
  140. package/dist/view/index.d.ts.map +1 -0
  141. package/dist/view/index.js +7 -0
  142. package/dist/view/index.js.map +1 -0
  143. package/dist/view/protocol.d.ts +220 -0
  144. package/dist/view/protocol.d.ts.map +1 -0
  145. package/dist/view/protocol.js +246 -0
  146. package/dist/view/protocol.js.map +1 -0
  147. package/dist/view/reference.d.ts +18 -0
  148. package/dist/view/reference.d.ts.map +1 -0
  149. package/dist/view/reference.js +25 -0
  150. package/dist/view/reference.js.map +1 -0
  151. package/dist/view/resources.d.ts +51 -0
  152. package/dist/view/resources.d.ts.map +1 -0
  153. package/dist/view/resources.js +242 -0
  154. package/dist/view/resources.js.map +1 -0
  155. package/dist/view/sanitize.d.ts +32 -0
  156. package/dist/view/sanitize.d.ts.map +1 -0
  157. package/dist/view/sanitize.js +226 -0
  158. package/dist/view/sanitize.js.map +1 -0
  159. package/dist/zip/central.d.ts +12 -0
  160. package/dist/zip/central.d.ts.map +1 -0
  161. package/dist/zip/central.js +72 -0
  162. package/dist/zip/central.js.map +1 -0
  163. package/dist/zip/crc32.d.ts +3 -0
  164. package/dist/zip/crc32.d.ts.map +1 -0
  165. package/dist/zip/crc32.js +28 -0
  166. package/dist/zip/crc32.js.map +1 -0
  167. package/dist/zip/eocd.d.ts +9 -0
  168. package/dist/zip/eocd.d.ts.map +1 -0
  169. package/dist/zip/eocd.js +84 -0
  170. package/dist/zip/eocd.js.map +1 -0
  171. package/dist/zip/errors.d.ts +19 -0
  172. package/dist/zip/errors.d.ts.map +1 -0
  173. package/dist/zip/errors.js +37 -0
  174. package/dist/zip/errors.js.map +1 -0
  175. package/dist/zip/index.d.ts +18 -0
  176. package/dist/zip/index.d.ts.map +1 -0
  177. package/dist/zip/index.js +78 -0
  178. package/dist/zip/index.js.map +1 -0
  179. package/dist/zip/inflate.d.ts +2 -0
  180. package/dist/zip/inflate.d.ts.map +1 -0
  181. package/dist/zip/inflate.js +43 -0
  182. package/dist/zip/inflate.js.map +1 -0
  183. package/dist/zip/source.d.ts +15 -0
  184. package/dist/zip/source.d.ts.map +1 -0
  185. package/dist/zip/source.js +34 -0
  186. package/dist/zip/source.js.map +1 -0
  187. package/package.json +75 -0
  188. package/src/core/book.ts +80 -0
  189. package/src/core/errors.ts +31 -0
  190. package/src/core/format.ts +14 -0
  191. package/src/core/graphemes.ts +73 -0
  192. package/src/core/index.ts +27 -0
  193. package/src/core/lookup.ts +16 -0
  194. package/src/core/open.ts +31 -0
  195. package/src/core/position.ts +485 -0
  196. package/src/core/reading-text.ts +200 -0
  197. package/src/core/source.ts +61 -0
  198. package/src/core/storage.ts +6 -0
  199. package/src/core/text.ts +35 -0
  200. package/src/formats/epub/href.ts +54 -0
  201. package/src/formats/epub/index.ts +235 -0
  202. package/src/formats/epub/opf.ts +143 -0
  203. package/src/formats/epub/toc.ts +120 -0
  204. package/src/formats/fb2/index.ts +348 -0
  205. package/src/formats/index.ts +3 -0
  206. package/src/formats/text/index.ts +208 -0
  207. package/src/formats/xml.ts +326 -0
  208. package/src/index.ts +26 -0
  209. package/src/layout/chunk.ts +169 -0
  210. package/src/layout/index.ts +573 -0
  211. package/src/reader/index.ts +1206 -0
  212. package/src/reader/input.ts +122 -0
  213. package/src/search/extract.ts +403 -0
  214. package/src/search/index.ts +10 -0
  215. package/src/search/matcher.ts +187 -0
  216. package/src/search/normalize.ts +122 -0
  217. package/src/view/allowlist.ts +273 -0
  218. package/src/view/appearance.ts +380 -0
  219. package/src/view/css.ts +172 -0
  220. package/src/view/frame.ts +1018 -0
  221. package/src/view/host.ts +527 -0
  222. package/src/view/index.ts +24 -0
  223. package/src/view/protocol.ts +412 -0
  224. package/src/view/reference.ts +30 -0
  225. package/src/view/resources.ts +265 -0
  226. package/src/view/sanitize.ts +266 -0
  227. package/src/zip/central.ts +85 -0
  228. package/src/zip/crc32.ts +30 -0
  229. package/src/zip/eocd.ts +97 -0
  230. package/src/zip/errors.ts +41 -0
  231. package/src/zip/index.ts +114 -0
  232. package/src/zip/inflate.ts +41 -0
  233. package/src/zip/source.ts +51 -0
@@ -0,0 +1,97 @@
1
+ import { ZipFormatError } from './errors.ts';
2
+ import { readExact, view, type ByteSource } from './source.ts';
3
+
4
+ const EOCD_SIG = 0x06054b50;
5
+ const EOCD_MIN = 22;
6
+ const MAX_COMMENT = 0xffff;
7
+ const LOCATOR_SIG = 0x07064b50;
8
+ const LOCATOR_SIZE = 20;
9
+ const ZIP64_EOCD_SIG = 0x06064b50;
10
+ const ZIP64_EOCD_MIN = 56;
11
+ const INITIAL_PROBE = 1024;
12
+
13
+ export interface CentralDirectoryLocation {
14
+ offset: number;
15
+ size: number;
16
+ entryCount: number;
17
+ }
18
+
19
+ export async function locateCentralDirectory(src: ByteSource): Promise<CentralDirectoryLocation> {
20
+ if (src.size < EOCD_MIN) throw new ZipFormatError('too small to be a zip archive');
21
+
22
+ let probe = Math.min(src.size, INITIAL_PROBE);
23
+ let tail = await readExact(src, src.size - probe, probe);
24
+ let idx = findEocd(tail);
25
+ const maxProbe = Math.min(src.size, EOCD_MIN + MAX_COMMENT);
26
+ if (idx < 0 && probe < maxProbe) {
27
+ probe = maxProbe;
28
+ tail = await readExact(src, src.size - probe, probe);
29
+ idx = findEocd(tail);
30
+ }
31
+ if (idx < 0) throw new ZipFormatError('end of central directory record not found');
32
+
33
+ const eocdFileOffset = src.size - probe + idx;
34
+ const zip64 = await readZip64(src, tail, idx, eocdFileOffset);
35
+ if (zip64) return zip64;
36
+
37
+ const dv = view(tail);
38
+ const diskNumber = dv.getUint16(idx + 4, true);
39
+ const cdDisk = dv.getUint16(idx + 6, true);
40
+ if (diskNumber !== 0 || cdDisk !== 0) {
41
+ throw new ZipFormatError('multi-disk archives are not supported');
42
+ }
43
+ const entryCount = dv.getUint16(idx + 10, true);
44
+ const size = dv.getUint32(idx + 12, true);
45
+ const offset = dv.getUint32(idx + 16, true);
46
+ if (entryCount === 0xffff || size === 0xffffffff || offset === 0xffffffff) {
47
+ throw new ZipFormatError('zip64 markers present but the zip64 locator is missing');
48
+ }
49
+ return { offset, size, entryCount };
50
+ }
51
+
52
+ function findEocd(tail: Uint8Array): number {
53
+ const dv = view(tail);
54
+ for (let i = tail.byteLength - EOCD_MIN; i >= 0; i--) {
55
+ if (dv.getUint32(i, true) !== EOCD_SIG) continue;
56
+ const commentLen = dv.getUint16(i + 20, true);
57
+ if (i + EOCD_MIN + commentLen === tail.byteLength) return i;
58
+ }
59
+ return -1;
60
+ }
61
+
62
+ async function readZip64(
63
+ src: ByteSource,
64
+ tail: Uint8Array,
65
+ eocdIdx: number,
66
+ eocdFileOffset: number,
67
+ ): Promise<CentralDirectoryLocation | null> {
68
+ if (eocdFileOffset < LOCATOR_SIZE) return null;
69
+ const locator =
70
+ eocdIdx >= LOCATOR_SIZE
71
+ ? tail.subarray(eocdIdx - LOCATOR_SIZE, eocdIdx)
72
+ : await readExact(src, eocdFileOffset - LOCATOR_SIZE, LOCATOR_SIZE);
73
+ const ldv = view(locator);
74
+ if (ldv.getUint32(0, true) !== LOCATOR_SIG) return null;
75
+ if (ldv.getUint32(16, true) > 1) {
76
+ throw new ZipFormatError('multi-disk archives are not supported');
77
+ }
78
+ const recordOffset = toSafeNumber(ldv.getBigUint64(8, true), 'zip64 EOCD offset');
79
+
80
+ const record = await readExact(src, recordOffset, ZIP64_EOCD_MIN);
81
+ const rdv = view(record);
82
+ if (rdv.getUint32(0, true) !== ZIP64_EOCD_SIG) {
83
+ throw new ZipFormatError('zip64 locator points at a missing zip64 EOCD record');
84
+ }
85
+ return {
86
+ entryCount: toSafeNumber(rdv.getBigUint64(32, true), 'zip64 entry count'),
87
+ size: toSafeNumber(rdv.getBigUint64(40, true), 'zip64 central directory size'),
88
+ offset: toSafeNumber(rdv.getBigUint64(48, true), 'zip64 central directory offset'),
89
+ };
90
+ }
91
+
92
+ export function toSafeNumber(value: bigint, what: string): number {
93
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
94
+ throw new ZipFormatError(`${what} exceeds the safe integer range`);
95
+ }
96
+ return Number(value);
97
+ }
@@ -0,0 +1,41 @@
1
+ export class ZipError extends Error {
2
+ constructor(message: string, options?: ErrorOptions) {
3
+ super(message, options);
4
+ this.name = 'ZipError';
5
+ }
6
+ }
7
+
8
+ export class ZipFormatError extends ZipError {
9
+ constructor(message: string, options?: ErrorOptions) {
10
+ super(message, options);
11
+ this.name = 'ZipFormatError';
12
+ }
13
+ }
14
+
15
+ export class ZipCrcMismatchError extends ZipFormatError {
16
+ constructor(message: string, options?: ErrorOptions) {
17
+ super(message, options);
18
+ this.name = 'ZipCrcMismatchError';
19
+ }
20
+ }
21
+
22
+ export class ZipEncryptedEntryError extends ZipError {
23
+ constructor(message: string, options?: ErrorOptions) {
24
+ super(message, options);
25
+ this.name = 'ZipEncryptedEntryError';
26
+ }
27
+ }
28
+
29
+ export class ZipUnsupportedMethodError extends ZipError {
30
+ constructor(message: string, options?: ErrorOptions) {
31
+ super(message, options);
32
+ this.name = 'ZipUnsupportedMethodError';
33
+ }
34
+ }
35
+
36
+ export class ZipEntryNotFoundError extends ZipError {
37
+ constructor(message: string, options?: ErrorOptions) {
38
+ super(message, options);
39
+ this.name = 'ZipEntryNotFoundError';
40
+ }
41
+ }
@@ -0,0 +1,114 @@
1
+ import { parseCentralDirectory, type CentralRecord } from './central.ts';
2
+ import { crc32 } from './crc32.ts';
3
+ import { locateCentralDirectory } from './eocd.ts';
4
+ import {
5
+ ZipCrcMismatchError,
6
+ ZipEncryptedEntryError,
7
+ ZipEntryNotFoundError,
8
+ ZipError,
9
+ ZipFormatError,
10
+ ZipUnsupportedMethodError,
11
+ } from './errors.ts';
12
+ import { decompress } from './inflate.ts';
13
+ import { readExact, toByteSource, view, type ByteSource, type ZipSource } from './source.ts';
14
+
15
+ export {
16
+ ZipCrcMismatchError,
17
+ ZipEncryptedEntryError,
18
+ ZipEntryNotFoundError,
19
+ ZipError,
20
+ ZipFormatError,
21
+ ZipUnsupportedMethodError,
22
+ };
23
+ export type { RangeReader, RangeReaderSource, ZipSource } from './source.ts';
24
+
25
+ const LOCAL_SIG = 0x04034b50;
26
+ const LOCAL_HEADER_SIZE = 30;
27
+
28
+ function hex32(value: number): string {
29
+ return `0x${value.toString(16).padStart(8, '0')}`;
30
+ }
31
+
32
+ export interface ZipEntry {
33
+ readonly name: string;
34
+ readonly method: number;
35
+ readonly compressedSize: number;
36
+ readonly uncompressedSize: number;
37
+ readonly encrypted: boolean;
38
+ }
39
+
40
+ export interface ZipArchive {
41
+ readonly entries: readonly ZipEntry[];
42
+ entry(name: string): ZipEntry | undefined;
43
+ read(name: string): Promise<Uint8Array>;
44
+ }
45
+
46
+ export async function openZip(source: ZipSource): Promise<ZipArchive> {
47
+ const src = toByteSource(source);
48
+ const location = await locateCentralDirectory(src);
49
+ const directory = await readExact(src, location.offset, location.size);
50
+ const records = parseCentralDirectory(directory, location.entryCount);
51
+ const byName = new Map<string, CentralRecord>();
52
+ for (const record of records) byName.set(record.name, record);
53
+
54
+ const entries: readonly ZipEntry[] = records.map((record) => ({
55
+ name: record.name,
56
+ method: record.method,
57
+ compressedSize: record.compressedSize,
58
+ uncompressedSize: record.uncompressedSize,
59
+ encrypted: record.encrypted,
60
+ }));
61
+ // First entry wins on a duplicate name, preserving the Array.find semantics
62
+ // this Map lookup replaced.
63
+ const entryByName = new Map<string, ZipEntry>();
64
+ for (const entry of entries) {
65
+ if (!entryByName.has(entry.name)) entryByName.set(entry.name, entry);
66
+ }
67
+
68
+ return {
69
+ entries,
70
+ entry: (name) => entryByName.get(name),
71
+ read: async (name) => {
72
+ const record = byName.get(name);
73
+ if (!record) throw new ZipEntryNotFoundError(`no entry named ${name}`);
74
+ return readEntry(src, record);
75
+ },
76
+ };
77
+ }
78
+
79
+ async function readEntry(src: ByteSource, record: CentralRecord): Promise<Uint8Array> {
80
+ if (record.encrypted) {
81
+ throw new ZipEncryptedEntryError(`entry ${record.name} is encrypted; encrypted archives are not supported`);
82
+ }
83
+ const header = await readExact(src, record.localHeaderOffset, LOCAL_HEADER_SIZE);
84
+ const dv = view(header);
85
+ if (dv.getUint32(0, true) !== LOCAL_SIG) {
86
+ throw new ZipFormatError(`entry ${record.name}: no local file header at offset ${record.localHeaderOffset}`);
87
+ }
88
+ // The local header's own name/extra lengths can differ from the central
89
+ // directory's (bsdtar pads local extras); sizes with data descriptors (flag
90
+ // bit 3) are zero here — the central directory values are authoritative.
91
+ const nameLen = dv.getUint16(26, true);
92
+ const extraLen = dv.getUint16(28, true);
93
+ const dataOffset = record.localHeaderOffset + LOCAL_HEADER_SIZE + nameLen + extraLen;
94
+ const data = await readExact(src, dataOffset, record.compressedSize);
95
+ const out = await decompress(data, record.method, record.name);
96
+ if (out.byteLength !== record.uncompressedSize) {
97
+ throw new ZipFormatError(
98
+ `entry ${record.name}: expected ${record.uncompressedSize} bytes, decompressed to ${out.byteLength}`,
99
+ );
100
+ }
101
+ // Verify against the central directory's CRC-32, unconditionally. The
102
+ // data-descriptor exemption (APPNOTE 4.4.7) zeroes the CRC only in the
103
+ // *local* header of a flag-bit-3 entry — "the correct value is put in the
104
+ // data descriptor and in the central directory" — so the central copy is
105
+ // always the real checksum and there is no CRC==0 skip: 0 is simply the
106
+ // legitimate CRC of an empty payload, which trivially matches.
107
+ const actual = crc32(out);
108
+ if (actual !== record.crc32) {
109
+ throw new ZipCrcMismatchError(
110
+ `entry ${record.name}: CRC-32 mismatch (expected ${hex32(record.crc32)}, computed ${hex32(actual)})`,
111
+ );
112
+ }
113
+ return out;
114
+ }
@@ -0,0 +1,41 @@
1
+ import { ZipFormatError, ZipUnsupportedMethodError } from './errors.ts';
2
+
3
+ const STORED = 0;
4
+ const DEFLATED = 8;
5
+
6
+ export async function decompress(data: Uint8Array, method: number, name: string): Promise<Uint8Array> {
7
+ if (method === STORED) return data.slice();
8
+ if (method === DEFLATED) return inflateRaw(data, name);
9
+ throw new ZipUnsupportedMethodError(`entry ${name} uses unsupported compression method ${method}`);
10
+ }
11
+
12
+ async function inflateRaw(data: Uint8Array, name: string): Promise<Uint8Array> {
13
+ const input = new ReadableStream<BufferSource>({
14
+ start(controller) {
15
+ // slice() re-backs the view with a plain ArrayBuffer — a range reader may
16
+ // hand back a SharedArrayBuffer-backed view, which BufferSource excludes.
17
+ controller.enqueue(data.slice());
18
+ controller.close();
19
+ },
20
+ });
21
+ const chunks: Uint8Array[] = [];
22
+ let total = 0;
23
+ try {
24
+ const reader = input.pipeThrough(new DecompressionStream('deflate-raw')).getReader();
25
+ for (;;) {
26
+ const { done, value } = await reader.read();
27
+ if (done) break;
28
+ chunks.push(value);
29
+ total += value.byteLength;
30
+ }
31
+ } catch (cause) {
32
+ throw new ZipFormatError(`entry ${name}: corrupt deflate stream`, { cause });
33
+ }
34
+ const out = new Uint8Array(total);
35
+ let offset = 0;
36
+ for (const chunk of chunks) {
37
+ out.set(chunk, offset);
38
+ offset += chunk.byteLength;
39
+ }
40
+ return out;
41
+ }
@@ -0,0 +1,51 @@
1
+ import { ZipFormatError } from './errors.ts';
2
+
3
+ export type RangeReader = (offset: number, length: number) => Promise<Uint8Array>;
4
+
5
+ /** Streaming input: `size` is required because locating a zip's central directory starts from the file tail. */
6
+ export interface RangeReaderSource {
7
+ read: RangeReader;
8
+ size: number;
9
+ }
10
+
11
+ export type ZipSource = ArrayBuffer | Uint8Array | Blob | RangeReaderSource;
12
+
13
+ export interface ByteSource {
14
+ size: number;
15
+ read: RangeReader;
16
+ }
17
+
18
+ export function toByteSource(source: ZipSource): ByteSource {
19
+ if (source instanceof Uint8Array) return bufferSource(source);
20
+ if (source instanceof ArrayBuffer) return bufferSource(new Uint8Array(source));
21
+ if (source instanceof Blob) {
22
+ return {
23
+ size: source.size,
24
+ read: async (offset, length) =>
25
+ new Uint8Array(await source.slice(offset, offset + length).arrayBuffer()),
26
+ };
27
+ }
28
+ return { size: source.size, read: (offset, length) => source.read(offset, length) };
29
+ }
30
+
31
+ function bufferSource(bytes: Uint8Array): ByteSource {
32
+ return {
33
+ size: bytes.byteLength,
34
+ read: (offset, length) => Promise.resolve(bytes.subarray(offset, offset + length)),
35
+ };
36
+ }
37
+
38
+ export async function readExact(src: ByteSource, offset: number, length: number): Promise<Uint8Array> {
39
+ if (offset < 0 || length < 0 || offset + length > src.size) {
40
+ throw new ZipFormatError(`range ${offset}..${offset + length} is outside the ${src.size}-byte source`);
41
+ }
42
+ const bytes = await src.read(offset, length);
43
+ if (bytes.byteLength !== length) {
44
+ throw new ZipFormatError(`short read at ${offset}: wanted ${length} bytes, got ${bytes.byteLength}`);
45
+ }
46
+ return bytes;
47
+ }
48
+
49
+ export function view(bytes: Uint8Array): DataView {
50
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
51
+ }