OpenCode commited on
Commit ·
e16bd16
1
Parent(s): 0e9ba6a
Fix Reader source IDs and bound PDF opening reads
Browse files- app.py +16 -2
- static/reader-contract.js +16 -1
- static/reader.js +4 -18
- tests/reader-contract-vectors.json +18 -0
- tests/test_fastapi_endpoints.py +20 -0
- tests/test_reader_backend.py +6 -0
- tests/test_reader_pdf_loading.py +52 -0
- tests/test_reader_regression.js +13 -0
- tests/test_static_contract.py +3 -3
app.py
CHANGED
|
@@ -257,9 +257,23 @@ BROWSER_FILE_ITEM_KEY_MAP = {
|
|
| 257 |
READER_SHORT_ID_RE = re.compile(r"^[0-9a-z]{13}$")
|
| 258 |
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
def reader_short_id(value: str) -> str:
|
| 261 |
digest = 1469598103934665603
|
| 262 |
-
for byte in value.encode("utf-8"):
|
| 263 |
digest = ((digest ^ byte) * 1099511628211) & ((1 << 64) - 1)
|
| 264 |
return _base36(digest).rjust(13, "0")
|
| 265 |
|
|
@@ -1845,7 +1859,7 @@ async def api_reader_resolve(id: str = Query(..., min_length=13, max_length=13))
|
|
| 1845 |
for index in reader_source_id_indices.get(id, []):
|
| 1846 |
record = records[index]
|
| 1847 |
matches.append(reader_resolved_record(record, assets.get("f", {}).get(build_reader_asset_key(record), {})))
|
| 1848 |
-
if len({match["download"] for match in matches}) > 1:
|
| 1849 |
return JSONResponse({"error": "阅读 ID 不唯一"}, status_code=409)
|
| 1850 |
matches = matches[:1]
|
| 1851 |
if matches:
|
|
|
|
| 257 |
READER_SHORT_ID_RE = re.compile(r"^[0-9a-z]{13}$")
|
| 258 |
|
| 259 |
|
| 260 |
+
def canonical_reader_source_url(value: str) -> str:
|
| 261 |
+
# Match reader-contract.js byte normalization without decoding path separators
|
| 262 |
+
# or treating an encoded percent sign as a second escape sequence.
|
| 263 |
+
def path_replace(match):
|
| 264 |
+
def token_replace(token_match):
|
| 265 |
+
token = token_match.group()
|
| 266 |
+
if re.fullmatch(r"%[0-9a-fA-F]{2}", token):
|
| 267 |
+
char = chr(int(token[1:], 16))
|
| 268 |
+
return char if re.fullmatch(r"[A-Za-z0-9._~-]", char) else token.upper()
|
| 269 |
+
return quote(token, safe="")
|
| 270 |
+
return match[1] + re.sub(r"%[0-9a-fA-F]{2}|[^A-Za-z0-9._~/-]", token_replace, match[2])
|
| 271 |
+
return re.sub(r"^(https://huggingface\.co/datasets/VoiceOfML/)([^?#]*)", path_replace, value)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
def reader_short_id(value: str) -> str:
|
| 275 |
digest = 1469598103934665603
|
| 276 |
+
for byte in canonical_reader_source_url(value).encode("utf-8"):
|
| 277 |
digest = ((digest ^ byte) * 1099511628211) & ((1 << 64) - 1)
|
| 278 |
return _base36(digest).rjust(13, "0")
|
| 279 |
|
|
|
|
| 1859 |
for index in reader_source_id_indices.get(id, []):
|
| 1860 |
record = records[index]
|
| 1861 |
matches.append(reader_resolved_record(record, assets.get("f", {}).get(build_reader_asset_key(record), {})))
|
| 1862 |
+
if len({canonical_reader_source_url(match["download"]) for match in matches}) > 1:
|
| 1863 |
return JSONResponse({"error": "阅读 ID 不唯一"}, status_code=409)
|
| 1864 |
matches = matches[:1]
|
| 1865 |
if matches:
|
static/reader-contract.js
CHANGED
|
@@ -369,9 +369,23 @@
|
|
| 369 |
params.set("folder_url", record.FolderUrl || record.folderUrl);
|
| 370 |
return (basePath || "/static/reader.html") + "?" + params.toString();
|
| 371 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
function shortSourceId(value) {
|
| 373 |
let hash = 1469598103934665603n;
|
| 374 |
-
for (const byte of new TextEncoder().encode(value)) {
|
| 375 |
hash ^= BigInt(byte);
|
| 376 |
hash = BigInt.asUintN(64, hash * 1099511628211n);
|
| 377 |
}
|
|
@@ -389,6 +403,7 @@
|
|
| 389 |
isBucketPath,
|
| 390 |
pdfPageSource,
|
| 391 |
txtRelativePath,
|
|
|
|
| 392 |
shortSourceId,
|
| 393 |
readerUrl
|
| 394 |
});
|
|
|
|
| 369 |
params.set("folder_url", record.FolderUrl || record.folderUrl);
|
| 370 |
return (basePath || "/static/reader.html") + "?" + params.toString();
|
| 371 |
}
|
| 372 |
+
function canonicalSourceUrl(value) {
|
| 373 |
+
// Normalize path bytes, not decoded URLs: %2F must remain distinct from /,
|
| 374 |
+
// and %2528 must never become %28. Query/fragment delimiters stay intact.
|
| 375 |
+
return value.replace(/^(https:\/\/huggingface\.co\/datasets\/VoiceOfML\/)([^?#]*)/, (_, prefix, path) =>
|
| 376 |
+
prefix + path.replace(/%[0-9a-fA-F]{2}|[^A-Za-z0-9._~/-]/gu, (token) => {
|
| 377 |
+
if (/^%[0-9a-f]{2}$/i.test(token)) {
|
| 378 |
+
const char = String.fromCharCode(parseInt(token.slice(1), 16));
|
| 379 |
+
return /^[A-Za-z0-9._~-]$/.test(char) ? char : token.toUpperCase();
|
| 380 |
+
}
|
| 381 |
+
return encodeURIComponent(token).replace(/[!'()*]/g, (char) =>
|
| 382 |
+
"%" + char.charCodeAt(0).toString(16).toUpperCase());
|
| 383 |
+
})
|
| 384 |
+
);
|
| 385 |
+
}
|
| 386 |
function shortSourceId(value) {
|
| 387 |
let hash = 1469598103934665603n;
|
| 388 |
+
for (const byte of new TextEncoder().encode(canonicalSourceUrl(value))) {
|
| 389 |
hash ^= BigInt(byte);
|
| 390 |
hash = BigInt.asUintN(64, hash * 1099511628211n);
|
| 391 |
}
|
|
|
|
| 403 |
isBucketPath,
|
| 404 |
pdfPageSource,
|
| 405 |
txtRelativePath,
|
| 406 |
+
canonicalSourceUrl,
|
| 407 |
shortSourceId,
|
| 408 |
readerUrl
|
| 409 |
});
|
static/reader.js
CHANGED
|
@@ -3761,22 +3761,6 @@ function preloadPdfEngine() {
|
|
| 3761 |
if (!pdfEnginePreload) pdfEnginePreload = loadPdfEngine();
|
| 3762 |
return pdfEnginePreload;
|
| 3763 |
}
|
| 3764 |
-
function pdfRangeChunkSize() {
|
| 3765 |
-
try {
|
| 3766 |
-
// The range-optimized artifact has page-oriented object placement. A
|
| 3767 |
-
// smaller chunk avoids pulling unrelated objects before the first page;
|
| 3768 |
-
// original PDFs retain the conservative default because some require
|
| 3769 |
-
// larger sequential reads or the complete file.
|
| 3770 |
-
const source = decodeURIComponent(sourceUrl);
|
| 3771 |
-
if (
|
| 3772 |
-
/\/pdf-range-v1-[0-9a-f]{16}-(?:objects|objects-linearized)\/document\.pdf(?:$|[?#])/i.test(
|
| 3773 |
-
source
|
| 3774 |
-
)
|
| 3775 |
-
)
|
| 3776 |
-
return 262144;
|
| 3777 |
-
} catch (_) {}
|
| 3778 |
-
return 1048576;
|
| 3779 |
-
}
|
| 3780 |
async function loadPdfEngine() {
|
| 3781 |
for (let attempt = 0; attempt < 3; attempt++) {
|
| 3782 |
assertReaderActive();
|
|
@@ -3824,9 +3808,11 @@ function loadPdfDocument() {
|
|
| 3824 |
pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL;
|
| 3825 |
const options = (url) => ({
|
| 3826 |
url,
|
| 3827 |
-
|
|
|
|
|
|
|
| 3828 |
disableAutoFetch: true,
|
| 3829 |
-
disableStream:
|
| 3830 |
wasmUrl: PDFJS_WASM_URL,
|
| 3831 |
cMapUrl: PDFJS_CMAP_URL,
|
| 3832 |
cMapPacked: true,
|
|
|
|
| 3761 |
if (!pdfEnginePreload) pdfEnginePreload = loadPdfEngine();
|
| 3762 |
return pdfEnginePreload;
|
| 3763 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3764 |
async function loadPdfEngine() {
|
| 3765 |
for (let attempt = 0; attempt < 3; attempt++) {
|
| 3766 |
assertReaderActive();
|
|
|
|
| 3808 |
pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL;
|
| 3809 |
const options = (url) => ({
|
| 3810 |
url,
|
| 3811 |
+
// Bound demand reads for original and converted PDFs alike. Streaming
|
| 3812 |
+
// otherwise keeps the full download running alongside these ranges.
|
| 3813 |
+
rangeChunkSize: 262144,
|
| 3814 |
disableAutoFetch: true,
|
| 3815 |
+
disableStream: true,
|
| 3816 |
wasmUrl: PDFJS_WASM_URL,
|
| 3817 |
cMapUrl: PDFJS_CMAP_URL,
|
| 3818 |
cMapPacked: true,
|
tests/reader-contract-vectors.json
CHANGED
|
@@ -5,6 +5,24 @@
|
|
| 5 |
["书/😀.epub", "1tgzoefbsl8ia"],
|
| 6 |
["https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/a%20b.txt", "1ckyy8f8m7pim"]
|
| 7 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"ocr": [
|
| 9 |
[{"Folder": ["edition.v2"], "File": "scan", "Extension": ""}, "edition.v2/scan.txt"],
|
| 10 |
[{"Folder": ["edition.v2"], "File": "book.v1", "Extension": "pdf"}, "edition.v2/book.v1.txt"],
|
|
|
|
| 5 |
["书/😀.epub", "1tgzoefbsl8ia"],
|
| 6 |
["https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/a%20b.txt", "1ckyy8f8m7pim"]
|
| 7 |
],
|
| 8 |
+
"canonicalPaths": [
|
| 9 |
+
["Kſ.pdf", "%E2%84%AA%C5%BF.pdf"],
|
| 10 |
+
["a (1)!'*.pdf", "a%20%281%29%21%27%2A.pdf"],
|
| 11 |
+
["a%20%281%29%21%27%2a.pdf", "a%20%281%29%21%27%2A.pdf"],
|
| 12 |
+
["中文/😀.pdf", "%E4%B8%AD%E6%96%87/%F0%9F%98%80.pdf"],
|
| 13 |
+
["%e4%b8%ad%e6%96%87/%f0%9f%98%80.pdf", "%E4%B8%AD%E6%96%87/%F0%9F%98%80.pdf"],
|
| 14 |
+
["%41%7a%30%2d%2e%5f%7e.pdf", "Az0-._~.pdf"],
|
| 15 |
+
["a%2fb.pdf", "a%2Fb.pdf"],
|
| 16 |
+
["a/b.pdf", "a/b.pdf"],
|
| 17 |
+
["a%2528.pdf", "a%2528.pdf"],
|
| 18 |
+
["100% done%.pdf", "100%25%20done%25.pdf"],
|
| 19 |
+
["a+b;:@=&.pdf", "a%2Bb%3B%3A%40%3D%26.pdf"],
|
| 20 |
+
["a%23%3f.pdf?download=true#page=2", "a%23%3F.pdf?download=true#page=2"]
|
| 21 |
+
],
|
| 22 |
+
"reportedBook": {
|
| 23 |
+
"path": "马列之声ebook小组第九批成果集成(2018.9.1)/马列之声ebook小组第九批成果集成(2018.9.1)part 1a/2018.8 part 1/20世纪的社会主义 刘俊奇著 2007.09_c__.pdf",
|
| 24 |
+
"id": "398vk0yyy8m29"
|
| 25 |
+
},
|
| 26 |
"ocr": [
|
| 27 |
[{"Folder": ["edition.v2"], "File": "scan", "Extension": ""}, "edition.v2/scan.txt"],
|
| 28 |
[{"Folder": ["edition.v2"], "File": "book.v1", "Extension": "pdf"}, "edition.v2/book.v1.txt"],
|
tests/test_fastapi_endpoints.py
CHANGED
|
@@ -166,6 +166,26 @@ class FastApiEndpointTests(unittest.TestCase):
|
|
| 166 |
response = self.client.get("/api/reader-resolve", params={"id": "0ym7e7wbcxau0"})
|
| 167 |
self.assertEqual(response.status_code, 404)
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
def test_reader_resolve_uses_cached_assets_without_waiting_for_refresh(self):
|
| 170 |
app.records = [{"Repo": "VoiceOfML/Test", "File": "book29", "Extension": "txt", "Folder": []}]
|
| 171 |
app.build_indexes(build_words=False)
|
|
|
|
| 166 |
response = self.client.get("/api/reader-resolve", params={"id": "0ym7e7wbcxau0"})
|
| 167 |
self.assertEqual(response.status_code, 404)
|
| 168 |
|
| 169 |
+
def test_reader_source_id_normalizes_frontend_and_backend_paths(self):
|
| 170 |
+
folders = ["马列之声ebook小组第九批成果集成(2018.9.1)",
|
| 171 |
+
"马列之声ebook小组第九批成果集成(2018.9.1)part 1a", "2018.8 part 1"]
|
| 172 |
+
record = {"Repo": "VoiceOfML/VOMEBOOK", "Folder": folders,
|
| 173 |
+
"File": "20世纪的社会主义 刘俊奇著 2007.09_c__", "Extension": "pdf"}
|
| 174 |
+
canonical = app.build_record_link(record)
|
| 175 |
+
for link in (canonical, canonical.replace("%28", "(").replace("%29", ")")):
|
| 176 |
+
app.records = [{**record, "Link": link}]
|
| 177 |
+
app.build_indexes(build_words=False)
|
| 178 |
+
with mock.patch.object(app, "get_reader_assets", mock.AsyncMock(return_value={"v": 1, "f": {}})):
|
| 179 |
+
response = self.client.get("/api/reader-resolve", params={"id": "398vk0yyy8m29"})
|
| 180 |
+
self.assertEqual(response.status_code, 200)
|
| 181 |
+
self.assertEqual(response.json()["download"], link)
|
| 182 |
+
self.assertEqual(self.client.get("/api/reader-resolve", params={"id": "1np0zrhual4h1"}).status_code, 404)
|
| 183 |
+
# Different spellings of one source are not a hash collision.
|
| 184 |
+
app.records.append({**record, "Link": canonical})
|
| 185 |
+
app.build_indexes(build_words=False)
|
| 186 |
+
with mock.patch.object(app, "get_reader_assets", mock.AsyncMock(return_value={"v": 1, "f": {}})):
|
| 187 |
+
self.assertEqual(self.client.get("/api/reader-resolve", params={"id": "398vk0yyy8m29"}).status_code, 200)
|
| 188 |
+
|
| 189 |
def test_reader_resolve_uses_cached_assets_without_waiting_for_refresh(self):
|
| 190 |
app.records = [{"Repo": "VoiceOfML/Test", "File": "book29", "Extension": "txt", "Folder": []}]
|
| 191 |
app.build_indexes(build_words=False)
|
tests/test_reader_backend.py
CHANGED
|
@@ -57,6 +57,12 @@ class ReaderArchiveValidationTests(unittest.TestCase):
|
|
| 57 |
vectors = json.loads(Path(__file__).with_name("reader-contract-vectors.json").read_text())
|
| 58 |
for value, expected in vectors["ids"]:
|
| 59 |
self.assertEqual(app.reader_short_id(value), expected)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
for record, expected in vectors["ocr"]:
|
| 61 |
self.assertEqual(app.build_txt_relative_path(record), expected)
|
| 62 |
|
|
|
|
| 57 |
vectors = json.loads(Path(__file__).with_name("reader-contract-vectors.json").read_text())
|
| 58 |
for value, expected in vectors["ids"]:
|
| 59 |
self.assertEqual(app.reader_short_id(value), expected)
|
| 60 |
+
prefix = "https://huggingface.co/datasets/VoiceOfML/VOMEBOOK/resolve/main/"
|
| 61 |
+
for value, expected in vectors["canonicalPaths"]:
|
| 62 |
+
self.assertEqual(app.canonical_reader_source_url(prefix + value), prefix + expected)
|
| 63 |
+
self.assertEqual(app.canonical_reader_source_url(prefix + expected), prefix + expected)
|
| 64 |
+
self.assertEqual(app.reader_short_id(prefix + value), app.reader_short_id(prefix + expected))
|
| 65 |
+
self.assertEqual(app.reader_short_id(prefix + vectors["reportedBook"]["path"]), vectors["reportedBook"]["id"])
|
| 66 |
for record, expected in vectors["ocr"]:
|
| 67 |
self.assertEqual(app.build_txt_relative_path(record), expected)
|
| 68 |
|
tests/test_reader_pdf_loading.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
"""Actual module imports and PDF.js with cold cache and transport faults."""
|
| 2 |
import contextlib
|
| 3 |
import json
|
|
|
|
|
|
|
|
|
|
| 4 |
import unittest
|
| 5 |
import urllib.parse
|
| 6 |
from unittest.mock import patch
|
|
@@ -78,6 +81,55 @@ class PdfLoadingTests(unittest.TestCase):
|
|
| 78 |
return entries.findIndex((entry) => entry.name.includes('/api/reader-resolve?'));
|
| 79 |
}'''))
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
def test_published_range_artifact_selects_small_chunk(self):
|
| 82 |
source = 'https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/' \
|
| 83 |
'objects/aa/' + 'a' * 64 + '/pdf-range-v1-0123456789abcdef-objects/document.pdf'
|
|
|
|
| 1 |
"""Actual module imports and PDF.js with cold cache and transport faults."""
|
| 2 |
import contextlib
|
| 3 |
import json
|
| 4 |
+
import re
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
import unittest
|
| 8 |
import urllib.parse
|
| 9 |
from unittest.mock import patch
|
|
|
|
| 81 |
return entries.findIndex((entry) => entry.name.includes('/api/reader-resolve?'));
|
| 82 |
}'''))
|
| 83 |
|
| 84 |
+
def test_large_original_pdf_renders_using_bounded_ranges_and_stops_transfer(self):
|
| 85 |
+
# A valid large stream object before the xref, not padding after EOF.
|
| 86 |
+
small = minimal_pdf()
|
| 87 |
+
xref = small.index(b'xref\n')
|
| 88 |
+
attachment = b'x' * (4 * 1024 * 1024)
|
| 89 |
+
extra = b'6 0 obj\n<< /Length ' + str(len(attachment)).encode() + b' >>\nstream\n' + attachment + b'\nendstream\nendobj\n'
|
| 90 |
+
suffix = small[xref:].replace(b'0 6\n', b'0 7\n').replace(b'trailer\n', f'{xref:010d} 00000 n \ntrailer\n'.encode()).replace(b'/Size 6', b'/Size 7')
|
| 91 |
+
suffix = re.sub(rb'startxref\n\d+', b'startxref\n' + str(xref + len(extra)).encode(), suffix)
|
| 92 |
+
payload = small[:xref] + extra + suffix
|
| 93 |
+
requests, stopped = [], threading.Event()
|
| 94 |
+
original = StaticHandler.do_GET
|
| 95 |
+
def serve(handler):
|
| 96 |
+
if not handler.path.startswith('/range-fixture.pdf'):
|
| 97 |
+
return original(handler)
|
| 98 |
+
match = re.fullmatch(r'bytes=(\d+)-(\d*)', handler.headers.get('Range', ''))
|
| 99 |
+
start, end = (int(match[1]), min(int(match[2] or len(payload)-1), len(payload)-1)) if match else (0, len(payload)-1)
|
| 100 |
+
request = {'range': (start, end) if match else None, 'sent': 0}
|
| 101 |
+
requests.append(request)
|
| 102 |
+
handler.send_response(206 if match else 200)
|
| 103 |
+
handler.send_header('Content-Type', 'application/pdf')
|
| 104 |
+
handler.send_header('Content-Length', str(end-start+1))
|
| 105 |
+
handler.send_header('Accept-Ranges', 'bytes')
|
| 106 |
+
if match:
|
| 107 |
+
handler.send_header('Content-Range', f'bytes {start}-{end}/{len(payload)}')
|
| 108 |
+
handler.end_headers()
|
| 109 |
+
try:
|
| 110 |
+
for offset in range(start, end+1, 16384):
|
| 111 |
+
chunk = payload[offset:min(offset+16384, end+1)]
|
| 112 |
+
handler.wfile.write(chunk)
|
| 113 |
+
handler.wfile.flush()
|
| 114 |
+
request['sent'] += len(chunk)
|
| 115 |
+
time.sleep(.005)
|
| 116 |
+
except (BrokenPipeError, ConnectionResetError):
|
| 117 |
+
pass
|
| 118 |
+
finally:
|
| 119 |
+
if not match: stopped.set()
|
| 120 |
+
self.page.unroute('**/api/reader-content**')
|
| 121 |
+
self.page.add_init_script('''const originalFetch = window.fetch;
|
| 122 |
+
window.fetch = (url, init) => originalFetch(String(url).includes('/api/reader-content?') ? '/range-fixture.pdf' : url, init);''')
|
| 123 |
+
with patch.object(StaticHandler, 'do_GET', serve):
|
| 124 |
+
self.open()
|
| 125 |
+
self.ready()
|
| 126 |
+
self.assertTrue(stopped.wait(2), 'full-file transfer must be cancelled')
|
| 127 |
+
ranges = [item['range'] for item in requests if item['range']]
|
| 128 |
+
self.assertIn((0, 262143), ranges)
|
| 129 |
+
self.assertTrue(any(end == len(payload)-1 for _, end in ranges))
|
| 130 |
+
self.assertLess(sum(item['sent'] for item in requests), 1024 * 1024)
|
| 131 |
+
self.assertEqual(len(self.modules), 1)
|
| 132 |
+
|
| 133 |
def test_published_range_artifact_selects_small_chunk(self):
|
| 134 |
source = 'https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/' \
|
| 135 |
'objects/aa/' + 'a' * 64 + '/pdf-range-v1-0123456789abcdef-objects/document.pdf'
|
tests/test_reader_regression.js
CHANGED
|
@@ -14,6 +14,19 @@ vm.runInNewContext(contract, sandbox)
|
|
| 14 |
const assets = sandbox.self.VoiceOfMLReader
|
| 15 |
const vectors = require('./reader-contract-vectors.json')
|
| 16 |
for (const [value, expected] of vectors.ids) assert.strictEqual(assets.shortSourceId(value), expected)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
for (const [record, expected] of vectors.ocr) {
|
| 18 |
const relative = [...record.Folder, record.File + (record.Extension ? '.' + record.Extension : '')].join('/')
|
| 19 |
assert.strictEqual(assets.txtRelativePath(relative), expected)
|
|
|
|
| 14 |
const assets = sandbox.self.VoiceOfMLReader
|
| 15 |
const vectors = require('./reader-contract-vectors.json')
|
| 16 |
for (const [value, expected] of vectors.ids) assert.strictEqual(assets.shortSourceId(value), expected)
|
| 17 |
+
const idPrefix = 'https://huggingface.co/datasets/VoiceOfML/VOMEBOOK/resolve/main/'
|
| 18 |
+
for (const [value, expected] of vectors.canonicalPaths) {
|
| 19 |
+
assert.strictEqual(assets.canonicalSourceUrl(idPrefix + value), idPrefix + expected)
|
| 20 |
+
assert.strictEqual(assets.canonicalSourceUrl(idPrefix + expected), idPrefix + expected)
|
| 21 |
+
assert.strictEqual(assets.shortSourceId(idPrefix + value), assets.shortSourceId(idPrefix + expected))
|
| 22 |
+
}
|
| 23 |
+
for (const bookPath of [vectors.reportedBook.path, vectors.reportedBook.path.split('/').map(encodeURIComponent).join('/')]) {
|
| 24 |
+
const link = idPrefix + bookPath
|
| 25 |
+
assert.strictEqual(assets.shortSourceId(link), vectors.reportedBook.id)
|
| 26 |
+
assert.strictEqual(new URL(assets.readerUrl({ Link: link, Extension: 'pdf' }), 'https://site.test').searchParams.get('id'), vectors.reportedBook.id)
|
| 27 |
+
}
|
| 28 |
+
assert.notStrictEqual(assets.shortSourceId(idPrefix + 'a%2Fb.pdf'), assets.shortSourceId(idPrefix + 'a/b.pdf'))
|
| 29 |
+
assert.notStrictEqual(assets.shortSourceId(idPrefix + 'a%2528.pdf'), assets.shortSourceId(idPrefix + 'a%28.pdf'))
|
| 30 |
for (const [record, expected] of vectors.ocr) {
|
| 31 |
const relative = [...record.Folder, record.File + (record.Extension ? '.' + record.Extension : '')].join('/')
|
| 32 |
assert.strictEqual(assets.txtRelativePath(relative), expected)
|
tests/test_static_contract.py
CHANGED
|
@@ -74,10 +74,10 @@ class StaticFrontendContractTests(unittest.TestCase):
|
|
| 74 |
self.assertIn("backToTopPending = true", self.js)
|
| 75 |
self.assertIn("backToTopPending = false", self.js)
|
| 76 |
|
| 77 |
-
def
|
| 78 |
reader = (ROOT / "static/reader.js").read_text(encoding="utf-8")
|
| 79 |
self.assertIn("disableAutoFetch: true", reader)
|
| 80 |
-
self.assertIn("disableStream:
|
| 81 |
|
| 82 |
def test_desktop_random_actions_match_labels_and_empty_state_behavior(self):
|
| 83 |
self.assertIn('id="random-book-btn" class="random-txt-btn" title="随机下本书"', self.html)
|
|
@@ -162,7 +162,7 @@ class StaticFrontendContractTests(unittest.TestCase):
|
|
| 162 |
assert_source_contains(self, 'cMapUrl: PDFJS_CMAP_URL', reader)
|
| 163 |
assert_source_contains(self, 'standardFontDataUrl: PDFJS_STANDARD_FONT_URL', reader)
|
| 164 |
assert_source_contains(self, 'disableAutoFetch: true', reader)
|
| 165 |
-
assert_source_contains(self, 'disableStream:
|
| 166 |
self.assertIn('.reader-content[data-mode="pdf-pages"] .reader-page > img', reader_css)
|
| 167 |
self.assertIn("'wasm-unsafe-eval'", reader_html)
|
| 168 |
assert_source_contains(self, '"gb18030", "big5", "windows-1251", "windows-1252"', reader)
|
|
|
|
| 74 |
self.assertIn("backToTopPending = true", self.js)
|
| 75 |
self.assertIn("backToTopPending = false", self.js)
|
| 76 |
|
| 77 |
+
def test_pdf_reader_uses_demand_ranges_without_full_file_streaming(self):
|
| 78 |
reader = (ROOT / "static/reader.js").read_text(encoding="utf-8")
|
| 79 |
self.assertIn("disableAutoFetch: true", reader)
|
| 80 |
+
self.assertIn("disableStream: true", reader)
|
| 81 |
|
| 82 |
def test_desktop_random_actions_match_labels_and_empty_state_behavior(self):
|
| 83 |
self.assertIn('id="random-book-btn" class="random-txt-btn" title="随机下本书"', self.html)
|
|
|
|
| 162 |
assert_source_contains(self, 'cMapUrl: PDFJS_CMAP_URL', reader)
|
| 163 |
assert_source_contains(self, 'standardFontDataUrl: PDFJS_STANDARD_FONT_URL', reader)
|
| 164 |
assert_source_contains(self, 'disableAutoFetch: true', reader)
|
| 165 |
+
assert_source_contains(self, 'disableStream: true', reader)
|
| 166 |
self.assertIn('.reader-content[data-mode="pdf-pages"] .reader-page > img', reader_css)
|
| 167 |
self.assertIn("'wasm-unsafe-eval'", reader_html)
|
| 168 |
assert_source_contains(self, '"gb18030", "big5", "windows-1251", "windows-1252"', reader)
|