Download static/reader-security.js from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader-security.js
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader-security.js
-
curl -L -o reader-security.js https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader-security.js
15.6 kB
| (function (root) { | |
| "use strict"; | |
| const LIMITS = Object.freeze({ | |
| documentBytes: 512 * 1024 * 1024, | |
| archiveCompressedBytes: 512 * 1024 * 1024, | |
| archiveExpandedBytes: 1536 * 1024 * 1024, | |
| archiveEntries: 20000, | |
| archiveRatio: 200, | |
| manifestBytes: 8 * 1024 * 1024, | |
| pdfPages: 100000, | |
| chapters: 10000, | |
| chapterBytes: 8 * 1024 * 1024, | |
| chapterTotalBytes: 512 * 1024 * 1024 | |
| }); | |
| function error(code) { | |
| const value = new Error(code); | |
| value.code = code; | |
| return value; | |
| } | |
| function assertResponseSize(response, limit) { | |
| const length = Number(response?.headers?.get?.("content-length")); | |
| if (Number.isFinite(length) && length > limit) throw error("READER_RESOURCE_LIMIT"); | |
| return response; | |
| } | |
| function createByteBudget(limit = LIMITS.chapterTotalBytes) { | |
| if (!Number.isSafeInteger(limit) || limit < 0) throw new RangeError("Invalid byte budget"); | |
| let used = 0; | |
| function reserve(bytes) { | |
| if (!Number.isSafeInteger(bytes) || bytes < 0) throw new RangeError("Invalid byte count"); | |
| if (bytes > limit - used) throw error("READER_RESOURCE_LIMIT"); | |
| used += bytes; | |
| } | |
| function release(bytes) { | |
| if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > used) | |
| throw new RangeError("Invalid byte release"); | |
| used -= bytes; | |
| } | |
| return Object.freeze({ | |
| reserve, | |
| release, | |
| get used() { | |
| return used; | |
| }, | |
| get remaining() { | |
| return limit - used; | |
| } | |
| }); | |
| } | |
| // Failed reads release their reservations; successful reads retain them until | |
| // the caller releases discarded content. Share one budget across a book. | |
| async function readBytes(response, limit = LIMITS.documentBytes, budget) { | |
| let reader, | |
| reserved = 0; | |
| try { | |
| assertResponseSize(response, limit); | |
| if (!response.body?.getReader) { | |
| const bytes = new Uint8Array(await response.arrayBuffer()); | |
| if (bytes.byteLength > limit) throw error("READER_RESOURCE_LIMIT"); | |
| budget?.reserve(bytes.byteLength); | |
| return bytes; | |
| } | |
| reader = response.body.getReader(); | |
| const chunks = []; | |
| let length = 0; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| if (!value?.byteLength) continue; | |
| length += value.byteLength; | |
| if (length > limit) throw error("READER_RESOURCE_LIMIT"); | |
| budget?.reserve(value.byteLength); | |
| reserved += value.byteLength; | |
| chunks.push(value); | |
| } | |
| const output = new Uint8Array(length); | |
| let offset = 0; | |
| for (const chunk of chunks) { | |
| output.set(chunk, offset); | |
| offset += chunk.byteLength; | |
| } | |
| return output; | |
| } catch (reason) { | |
| if (budget && reserved) budget.release(reserved); | |
| try { | |
| if (reader) await reader.cancel(reason); | |
| else await response.body?.cancel?.(reason); | |
| } catch (_) {} | |
| throw reason; | |
| } finally { | |
| reader?.releaseLock?.(); | |
| } | |
| } | |
| async function readText(response, limit, encoding = "utf-8", budget) { | |
| const decoder = new TextDecoder(encoding); | |
| const bytes = await readBytes(response, limit, budget); | |
| try { | |
| return decoder.decode(bytes); | |
| } catch (reason) { | |
| budget?.release(bytes.byteLength); | |
| throw reason; | |
| } | |
| } | |
| function inspectZip(bytes, limits = LIMITS) { | |
| bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); | |
| if (bytes.byteLength > limits.archiveCompressedBytes) throw error("READER_ARCHIVE_LIMIT"); | |
| const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); | |
| let end = -1; | |
| // EOCD must end at EOF, including its optional comment. File contents are | |
| // never searched for central-directory entries. | |
| for ( | |
| let offset = bytes.byteLength - 22; | |
| offset >= Math.max(0, bytes.byteLength - 22 - 65535); | |
| offset-- | |
| ) { | |
| if ( | |
| view.getUint32(offset, true) === 0x06054b50 && | |
| offset + 22 + view.getUint16(offset + 20, true) === bytes.byteLength | |
| ) { | |
| end = offset; | |
| break; | |
| } | |
| } | |
| if (end < 0) throw error("READER_ARCHIVE_INVALID"); | |
| const disk = view.getUint16(end + 4, true), | |
| directoryDisk = view.getUint16(end + 6, true); | |
| const diskEntries = view.getUint16(end + 8, true), | |
| entries = view.getUint16(end + 10, true); | |
| const directorySize = view.getUint32(end + 12, true), | |
| directoryOffset = view.getUint32(end + 16, true); | |
| if ( | |
| disk === 0xffff || | |
| directoryDisk === 0xffff || | |
| diskEntries === 0xffff || | |
| entries === 0xffff || | |
| directorySize === 0xffffffff || | |
| directoryOffset === 0xffffffff || | |
| (end >= 20 && view.getUint32(end - 20, true) === 0x07064b50) | |
| ) { | |
| throw error("READER_ARCHIVE_ZIP64_UNSUPPORTED"); | |
| } | |
| if (disk || directoryDisk || diskEntries !== entries) | |
| throw error("READER_ARCHIVE_MULTIDISK_UNSUPPORTED"); | |
| if (!entries || directoryOffset + directorySize !== end) throw error("READER_ARCHIVE_INVALID"); | |
| if (entries > limits.archiveEntries) throw error("READER_ARCHIVE_LIMIT"); | |
| let offset = directoryOffset, | |
| compressed = 0, | |
| expanded = 0; | |
| for (let index = 0; index < entries; index++) { | |
| if (offset + 46 > end || view.getUint32(offset, true) !== 0x02014b50) | |
| throw error("READER_ARCHIVE_INVALID"); | |
| const packed = view.getUint32(offset + 20, true), | |
| unpacked = view.getUint32(offset + 24, true); | |
| const nameLength = view.getUint16(offset + 28, true), | |
| extraLength = view.getUint16(offset + 30, true), | |
| commentLength = view.getUint16(offset + 32, true); | |
| const startDisk = view.getUint16(offset + 34, true), | |
| localOffset = view.getUint32(offset + 42, true); | |
| if ( | |
| packed === 0xffffffff || | |
| unpacked === 0xffffffff || | |
| localOffset === 0xffffffff || | |
| startDisk === 0xffff | |
| ) { | |
| throw error("READER_ARCHIVE_ZIP64_UNSUPPORTED"); | |
| } | |
| if (startDisk) throw error("READER_ARCHIVE_MULTIDISK_UNSUPPORTED"); | |
| const next = offset + 46 + nameLength + extraLength + commentLength; | |
| if (!nameLength || next > end) throw error("READER_ARCHIVE_INVALID"); | |
| const extraEnd = offset + 46 + nameLength + extraLength; | |
| for (let extra = offset + 46 + nameLength; extra < extraEnd; ) { | |
| if (extra + 4 > extraEnd) throw error("READER_ARCHIVE_INVALID"); | |
| if (view.getUint16(extra, true) === 1) throw error("READER_ARCHIVE_ZIP64_UNSUPPORTED"); | |
| extra += 4 + view.getUint16(extra + 2, true); | |
| if (extra > extraEnd) throw error("READER_ARCHIVE_INVALID"); | |
| } | |
| if (localOffset + 30 > directoryOffset || view.getUint32(localOffset, true) !== 0x04034b50) | |
| throw error("READER_ARCHIVE_INVALID"); | |
| const dataOffset = | |
| localOffset + | |
| 30 + | |
| view.getUint16(localOffset + 26, true) + | |
| view.getUint16(localOffset + 28, true); | |
| if (dataOffset + packed > directoryOffset) throw error("READER_ARCHIVE_INVALID"); | |
| compressed += packed; | |
| expanded += unpacked; | |
| if ( | |
| expanded > limits.archiveExpandedBytes || | |
| (packed === 0 ? unpacked > 0 : unpacked / packed > limits.archiveRatio) | |
| ) { | |
| throw error("READER_ARCHIVE_LIMIT"); | |
| } | |
| offset = next; | |
| } | |
| if (offset !== end) throw error("READER_ARCHIVE_INVALID"); | |
| return Object.freeze({ entries, compressed, expanded }); | |
| } | |
| function validateChapterManifest(manifest) { | |
| if ( | |
| manifest?.version !== 1 || | |
| manifest?.kind !== "epub-chapters" || | |
| !Array.isArray(manifest.chapters) || | |
| !manifest.chapters.length | |
| ) | |
| throw error("EPUB_INVALID"); | |
| if (manifest.chapters.length > LIMITS.chapters) throw error("READER_RESOURCE_LIMIT"); | |
| let declaredBytes = 0, | |
| expectedIndex = 1; | |
| for (const chapter of manifest.chapters) { | |
| if (!chapter || !Number.isInteger(chapter.index) || chapter.index !== expectedIndex) | |
| throw error("EPUB_INVALID"); | |
| expectedIndex += 1; | |
| if ( | |
| typeof chapter.path !== "string" || | |
| !chapter.path.trim() || | |
| /^[a-z][a-z0-9+.-]*:/i.test(chapter.path) || | |
| chapter.path.startsWith("/") || | |
| chapter.path.includes("\\") || | |
| chapter.path.split("/").some((part) => part === ".." || part === "." || !part) | |
| ) | |
| throw error("EPUB_INVALID"); | |
| const rawSize = chapter.bytes ?? chapter.size; | |
| const size = rawSize == null ? 0 : Number(rawSize); | |
| if (!Number.isFinite(size) || size < 0 || size > LIMITS.chapterBytes) | |
| throw error("READER_RESOURCE_LIMIT"); | |
| declaredBytes += size; | |
| } | |
| if (declaredBytes > LIMITS.chapterTotalBytes) throw error("READER_RESOURCE_LIMIT"); | |
| if (manifest.toc !== undefined) { | |
| if (!Array.isArray(manifest.toc)) throw error("EPUB_INVALID"); | |
| for (const item of manifest.toc) | |
| if (!item || typeof item.title !== "string" || !item.title.trim() || | |
| !Number.isInteger(item.chapter) || item.chapter < 1 || item.chapter > manifest.chapters.length || | |
| !Number.isInteger(item.depth) || item.depth < 0 || item.depth > 64 || | |
| typeof item.fragment !== "string") throw error("EPUB_INVALID"); | |
| } | |
| return manifest; | |
| } | |
| function validatePdfPageManifest(manifest) { | |
| if (!manifest || manifest.version !== 2 || manifest.kind !== "pdf-pages") | |
| throw error("PDF_MANIFEST_INVALID"); | |
| if (!Number.isInteger(manifest.page_count) || manifest.page_count < 1 || manifest.pages !== undefined) | |
| throw error("PDF_MANIFEST_INVALID"); | |
| const pageCount = manifest.page_count; | |
| if (pageCount > LIMITS.pdfPages) throw error("READER_RESOURCE_LIMIT"); | |
| if (manifest.toc !== undefined) { | |
| if (!Array.isArray(manifest.toc) || manifest.toc.length > 2000) | |
| throw error("PDF_MANIFEST_INVALID"); | |
| for (const item of manifest.toc) { | |
| if ( | |
| !item || | |
| typeof item.title !== "string" || | |
| !item.title.trim() || | |
| item.title.length > 500 || | |
| !Number.isInteger(item.page) || | |
| item.page < 1 || | |
| item.page > pageCount || | |
| !Number.isInteger(item.depth) || | |
| item.depth < 0 || | |
| item.depth > 32 | |
| ) | |
| throw error("PDF_MANIFEST_INVALID"); | |
| } | |
| } | |
| if (manifest.ocr !== undefined) { | |
| if (!Array.isArray(manifest.ocr) || manifest.ocr.length !== pageCount) | |
| throw error("PDF_MANIFEST_INVALID"); | |
| for (const [index, page] of manifest.ocr.entries()) { | |
| const expected = String(index + 1).padStart(6, "0"), | |
| root = typeof page?.o === "string" ? PDF_OCR_ROOT.exec(page.o)?.[1] || "" : ""; | |
| if (!page || page.p !== index + 1 || !PDF_OCR_PATH.test(page.o) || | |
| page.o !== `${root}/ocr/page-${expected}.json.gz` || | |
| pdfSourceRoot(root) !== `objects/${manifest.source_sha256.slice(0, 2)}/${manifest.source_sha256}` || | |
| !/^[0-9a-f]{64}$/.test(page.os || "") || | |
| !Number.isSafeInteger(page.ob) || page.ob < 1) | |
| throw error("PDF_MANIFEST_INVALID"); | |
| } | |
| } | |
| return manifest; | |
| } | |
| const PDF_OCR_PATH = /^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[0-9a-f]{16}\/)?ocr\/(?:page-[0-9]{6}\.json\.gz|book-text\.json\.gz)$/; | |
| const PDF_OCR_MANIFEST_PATH = /^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[0-9a-f]{16}\/)?ocr-manifest\.json$/; | |
| const PDF_OCR_PAGE_PATH = /^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[0-9a-f]{16}\/)?pages\/page-[0-9]{6}\.(?:webp|jxl)$/; | |
| const PDF_OCR_ROOT = /^(objects\/[0-9a-f]{2}\/[0-9a-f]{64}(?:\/[0-9a-f]{16})?)/; | |
| function pdfSourceRoot(path) { | |
| return typeof path === "string" ? path.split("/").slice(0, 3).join("/") : ""; | |
| } | |
| function validatePdfOcrManifest(manifest) { | |
| if (!manifest || manifest.version !== 1 || manifest.kind !== "pdf-ocr" || | |
| manifest.complete !== true || !/^[0-9a-f]{64}$/.test(manifest.source_sha256 || "") || | |
| !/layout-v\d+(?:-|$)/.test(String(manifest.profile || "")) || | |
| !Number.isInteger(manifest.page_count) || manifest.page_count < 1 || | |
| manifest.page_count > LIMITS.pdfPages || !Array.isArray(manifest.pages) || | |
| manifest.pages.length !== manifest.page_count) | |
| throw error("PDF_OCR_MANIFEST_INVALID"); | |
| let root = ""; | |
| for (const [index, page] of manifest.pages.entries()) { | |
| const expected = String(index + 1).padStart(6, "0"); | |
| const pageRoot = typeof page?.o === "string" ? PDF_OCR_ROOT.exec(page.o)?.[1] || "" : ""; | |
| if (!page || page.p !== index + 1 || !PDF_OCR_PATH.test(page.o) || | |
| page.o !== `${pageRoot}/ocr/page-${expected}.json.gz` || | |
| (page.w !== undefined && (typeof page.w !== "string" || !PDF_OCR_PAGE_PATH.test(page.w) || | |
| !page.w.endsWith(`/pages/page-${expected}.webp`) || | |
| pdfSourceRoot(page.w) !== pdfSourceRoot(pageRoot))) || | |
| (page.j !== undefined && (typeof page.j !== "string" || !PDF_OCR_PAGE_PATH.test(page.j) || | |
| !page.j.endsWith(`/pages/page-${expected}.jxl`) || | |
| pdfSourceRoot(page.j) !== pdfSourceRoot(pageRoot))) || | |
| pdfSourceRoot(pageRoot) !== `objects/${manifest.source_sha256.slice(0, 2)}/${manifest.source_sha256}` || | |
| (root && pdfSourceRoot(root) !== pdfSourceRoot(pageRoot))) | |
| throw error("PDF_OCR_MANIFEST_INVALID"); | |
| root = pageRoot; | |
| } | |
| if (!manifest.book_text || typeof manifest.book_text.path !== "string" || | |
| !PDF_OCR_PATH.test(manifest.book_text.path) || !manifest.book_text.path.endsWith('/ocr/book-text.json.gz') || | |
| pdfSourceRoot(manifest.book_text.path) !== pdfSourceRoot(root) || | |
| !/^[0-9a-f]{64}$/.test(manifest.book_text.sha256 || "") || | |
| !Number.isSafeInteger(manifest.book_text.bytes) || manifest.book_text.bytes < 1 || | |
| (manifest.page_manifest !== undefined && | |
| (!manifest.page_manifest || typeof manifest.page_manifest.path !== "string" || | |
| !/^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[0-9a-f]{16}\/)?page-manifest\.json$/.test(manifest.page_manifest.path) || | |
| pdfSourceRoot(manifest.page_manifest.path) !== pdfSourceRoot(root)))) | |
| throw error("PDF_OCR_MANIFEST_INVALID"); | |
| return manifest; | |
| } | |
| function validatePdfOcrPage(payload, pageNumber) { | |
| if (!payload || payload.version !== 1 || payload.kind !== "pdf-ocr-page" || | |
| payload.page !== pageNumber || !Array.isArray(payload.blocks) || payload.blocks.length > 10000 || | |
| !Number.isFinite(Number(payload.width)) || !Number.isFinite(Number(payload.height))) | |
| throw error("PDF_OCR_PAGE_INVALID"); | |
| for (const block of payload.blocks) { | |
| const box = block?.b; | |
| if (!block || typeof block.t !== "string" || block.t.length > 4096 || | |
| !Array.isArray(box) || box.length !== 4 || box.some((value) => !Number.isFinite(Number(value))) || | |
| box[0] < 0 || box[1] < 0 || box[2] > 1 || box[3] > 1 || box[2] < box[0] || box[3] < box[1]) | |
| throw error("PDF_OCR_PAGE_INVALID"); | |
| } | |
| return payload; | |
| } | |
| function isZipContainer(extension, bytes) { | |
| return ( | |
| ["epub", "fbz"].includes(String(extension || "").toLowerCase()) || | |
| !!(bytes && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) | |
| ); | |
| } | |
| root.VoiceOfMLReaderSecurity = Object.freeze({ | |
| LIMITS, | |
| assertResponseSize, | |
| createByteBudget, | |
| readBytes, | |
| readText, | |
| inspectZip, | |
| validateChapterManifest, | |
| validatePdfPageManifest, | |
| validatePdfOcrManifest, | |
| validatePdfOcrPage, | |
| isZipContainer | |
| }); | |
| })(typeof self !== "undefined" ? self : globalThis); | |