forked from 1b-agent/opencadstudio-upstream
fix: native-bpp decode accounting, GIF frame bombs, streaming remote bodies
A second adversarial round (with empirical reproduction against the vendored image 0.25) found the budget contract still had holes: - The probe billed 4 bytes/px, but 16-bit formats allocate their native layout during decode (8/px for RGBA16 PNG; TIFF stacks ~3x codec scratch on top). In-budget 16-bit images were falsely refused with self-contradictory notes, and the decode transient could exceed the budget the tiers claimed to enforce. The probe now reads the color type alongside the dimensions and bills the real peak (native layout + RGBA where they coexist, plus the TIFF codec factor), so refusals are honest and per-platform-correct, and the TIFF allowance follows the same accounting. - GIF: the probe reads the LOGICAL SCREEN, but a crafted frame may declare itself arbitrarily larger (empirically: a 10x10 probed screen with an 8192x8192 frame — 320 MB and 29 s from a probe-claimed-400-byte image). The GIF frame buffer is now capped to the probed size plus slack; frame bombs refuse like any other oversize. - Remote bodies on web streamed incrementally with the 64 MB cap enforced DURING the read (array_buffer() materialized a hostile chunked reply in full before any check could fire), aborting and cancelling the reader the moment the cap is crossed. - Degenerate raster entities (zero/non-finite size or vectors) no longer produce NaN placement math that aborted whole PDF exports — they resolve to the broken-reference placeholder. - Same-name resolution now has a deterministic winner (newest registration, via insertion order) instead of HashMap iteration order; URL query strings no longer break by-name matching; the image pick matches its refusal note by the quoted file name (substring collisions like a.tif/2011.tif attributed errors to the wrong file); a dropped remote outcome at the queue cap triggers one unconditional catch-up re-resolution instead of waiting for another event; the unknown-size refusal says so instead of printing 0x0; and the build script fails cleanly when there are no fonts to verify.
This commit is contained in:
1 parent
58d3789bcf
commit
c6db94e86f
27 files changed
+284
-50
No files matched your search
@@ -159,6 +159,8 @@ web-sys = { version = "=0.3.85", features = [
|
|||||||
"DataTransfer",
|
"DataTransfer",
|
||||||
"FileList",
|
"FileList",
|
||||||
"Event",
|
"Event",
|
||||||
|
"ReadableStreamDefaultReader",
|
||||||
|
"ReadableStream",
|
||||||
] }
|
] }
|
||||||
iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39", features = ["webgl", "fira-sans"] }
|
iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39", features = ["webgl", "fira-sans"] }
|
||||||
|
|
||||||
|
|||||||
@@ -3589,6 +3589,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = يجري فتح مناقشات OCS للمساعدة والأسئلة...
|
.opening-ocs-discussions-for-help-and-questions = يجري فتح مناقشات OCS للمساعدة والأسئلة...
|
||||||
.opening-ocs-web = يجري فتح OCS Web...
|
.opening-ocs-web = يجري فتح OCS Web...
|
||||||
.opening-the-command-reference = فتح مرجع الأوامر...
|
.opening-the-command-reference = فتح مرجع الأوامر...
|
||||||
|
.image-refusal-unknown-size = تعذّر قياس أبعاد "__ocs_fmt_0__" من ترويسته وسيتجاوز فك ترميزه حد الصور __ocs_fmt_1__ ميغابايت في هذا الإصدار — لا يمكن إرفاقه.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" محمّل بالفعل بمحتوى مختلف؛ تم الاحتفاظ بالملف الموجود.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" محمّل بالفعل بمحتوى مختلف؛ تم الاحتفاظ بالملف الموجود.
|
||||||
.opening-patreon-page = يجري فتح صفحة Patreon...
|
.opening-patreon-page = يجري فتح صفحة Patreon...
|
||||||
.opening-feedback-page = يجري فتح صفحة الملاحظات...
|
.opening-feedback-page = يجري فتح صفحة الملاحظات...
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Отваряне на OCS дискусии за помощ и въпроси...
|
.opening-ocs-discussions-for-help-and-questions = Отваряне на OCS дискусии за помощ и въпроси...
|
||||||
.opening-ocs-web = Отваряне на OCS уебсайта...
|
.opening-ocs-web = Отваряне на OCS уебсайта...
|
||||||
.opening-the-command-reference = Отваряне на справочника за команди...
|
.opening-the-command-reference = Отваряне на справочника за команди...
|
||||||
|
.image-refusal-unknown-size = Размерът на „__ocs_fmt_0__“ не можа да се измери от заглавната му част, а декодирането му би надвишило __ocs_fmt_1__ MB лимита на тази сборка — не може да се прикачи.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" вече е зареден с различно съдържание; съществуващият файл е запазен.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" вече е зареден с различно съдържание; съществуващият файл е запазен.
|
||||||
.opening-patreon-page = Отваряне на страницата в Patreon...
|
.opening-patreon-page = Отваряне на страницата в Patreon...
|
||||||
.opening-feedback-page = Отваряне на страницата за обратна връзка...
|
.opening-feedback-page = Отваряне на страницата за обратна връзка...
|
||||||
|
|||||||
@@ -3574,6 +3574,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Otevírání diskuzí OCS pro nápovědu a dotazy...
|
.opening-ocs-discussions-for-help-and-questions = Otevírání diskuzí OCS pro nápovědu a dotazy...
|
||||||
.opening-ocs-web = Otevírání webu OCS...
|
.opening-ocs-web = Otevírání webu OCS...
|
||||||
.opening-the-command-reference = Otevírání referenční příručky příkazů...
|
.opening-the-command-reference = Otevírání referenční příručky příkazů...
|
||||||
|
.image-refusal-unknown-size = „__ocs_fmt_0__“ nešlo změřit ze záhlaví a jeho dekódování by překročilo limit __ocs_fmt_1__ MB tohoto sestavení — nelze připojit.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" je již načten s jiným obsahem; existující soubor byl zachován.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" je již načten s jiným obsahem; existující soubor byl zachován.
|
||||||
.opening-patreon-page = Otevírání stránky Patreon...
|
.opening-patreon-page = Otevírání stránky Patreon...
|
||||||
.opening-feedback-page = Otevírá se stránka zpětné vazby...
|
.opening-feedback-page = Otevírá se stránka zpětné vazby...
|
||||||
|
|||||||
@@ -3575,6 +3575,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Eröffnung OCS Diskussionen für Hilfe und Fragen...
|
.opening-ocs-discussions-for-help-and-questions = Eröffnung OCS Diskussionen für Hilfe und Fragen...
|
||||||
.opening-ocs-web = Öffnen OCS Web...
|
.opening-ocs-web = Öffnen OCS Web...
|
||||||
.opening-the-command-reference = Öffnen der Befehlsreferenz...
|
.opening-the-command-reference = Öffnen der Befehlsreferenz...
|
||||||
|
.image-refusal-unknown-size = „__ocs_fmt_0__“ konnte anhand des Headers nicht vermessen werden, und das Dekodieren überschreitet das __ocs_fmt_1__-MB-Limit dieses Builds — kann nicht angehängt werden.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" ist bereits mit anderem Inhalt geladen; die vorhandene Datei wurde behalten.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" ist bereits mit anderem Inhalt geladen; die vorhandene Datei wurde behalten.
|
||||||
.opening-patreon-page = Eröffnungsseite Patreon...
|
.opening-patreon-page = Eröffnungsseite Patreon...
|
||||||
.opening-feedback-page = Feedbackseite öffnen...
|
.opening-feedback-page = Feedbackseite öffnen...
|
||||||
|
|||||||
@@ -3571,6 +3571,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Άνοιγμα των συζητήσεων OCS για βοήθεια και ερωτήσεις...
|
.opening-ocs-discussions-for-help-and-questions = Άνοιγμα των συζητήσεων OCS για βοήθεια και ερωτήσεις...
|
||||||
.opening-ocs-web = Άνοιγμα του OCS Web...
|
.opening-ocs-web = Άνοιγμα του OCS Web...
|
||||||
.opening-the-command-reference = Άνοιγμα της αναφοράς εντολών...
|
.opening-the-command-reference = Άνοιγμα της αναφοράς εντολών...
|
||||||
|
.image-refusal-unknown-size = Το "__ocs_fmt_0__" δεν μπόρεσε να μετρηθεί από την κεφαλίδα του και η αποκωδικοποίησή του θα ξεπερνούσε το όριο __ocs_fmt_1__ MB αυτής της build — δεν μπορεί να επισυναφθεί.
|
||||||
.already-loaded-different-contents-kept = Το "__ocs_fmt_0__" είναι ήδη φορτωμένο με διαφορετικό περιεχόμενο· διατηρήθηκε το υπάρχον αρχείο.
|
.already-loaded-different-contents-kept = Το "__ocs_fmt_0__" είναι ήδη φορτωμένο με διαφορετικό περιεχόμενο· διατηρήθηκε το υπάρχον αρχείο.
|
||||||
.opening-patreon-page = Άνοιγμα της σελίδας Patreon...
|
.opening-patreon-page = Άνοιγμα της σελίδας Patreon...
|
||||||
.opening-feedback-page = Άνοιγμα της σελίδας σχολίων...
|
.opening-feedback-page = Άνοιγμα της σελίδας σχολίων...
|
||||||
|
|||||||
@@ -3572,6 +3572,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Opening OCS Discussions for help and questions...
|
.opening-ocs-discussions-for-help-and-questions = Opening OCS Discussions for help and questions...
|
||||||
.opening-ocs-web = Opening OCS Web...
|
.opening-ocs-web = Opening OCS Web...
|
||||||
.opening-the-command-reference = Opening the command reference...
|
.opening-the-command-reference = Opening the command reference...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" could not be sized from its header and decoding it exceeded the __ocs_fmt_1__ MB image limit of this build — it cannot be attached.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" is already loaded with different contents; the existing file was kept.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" is already loaded with different contents; the existing file was kept.
|
||||||
.opening-patreon-page = Opening Patreon page...
|
.opening-patreon-page = Opening Patreon page...
|
||||||
.opening-feedback-page = Opening feedback page...
|
.opening-feedback-page = Opening feedback page...
|
||||||
|
|||||||
@@ -3577,6 +3577,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Abriendo Debates de OCS para obtener ayuda y hacer preguntas...
|
.opening-ocs-discussions-for-help-and-questions = Abriendo Debates de OCS para obtener ayuda y hacer preguntas...
|
||||||
.opening-ocs-web = Abriendo OCS Web...
|
.opening-ocs-web = Abriendo OCS Web...
|
||||||
.opening-the-command-reference = Abriendo la referencia de comandos...
|
.opening-the-command-reference = Abriendo la referencia de comandos...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" no pudo medirse por su cabecera y decodificarlo excedería el límite de __ocs_fmt_1__ MB de esta build — no se puede adjuntar.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" ya está cargado con contenido diferente; se conservó el archivo existente.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" ya está cargado con contenido diferente; se conservó el archivo existente.
|
||||||
.opening-patreon-page = Abriendo la página de Patreon...
|
.opening-patreon-page = Abriendo la página de Patreon...
|
||||||
.opening-feedback-page = Abriendo la página de comentarios...
|
.opening-feedback-page = Abriendo la página de comentarios...
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = OCS-keskustelujen avaaminen apua ja kysymyksiä varten...
|
.opening-ocs-discussions-for-help-and-questions = OCS-keskustelujen avaaminen apua ja kysymyksiä varten...
|
||||||
.opening-ocs-web = Avataan OCS Web...
|
.opening-ocs-web = Avataan OCS Web...
|
||||||
.opening-the-command-reference = Avataan komentoviittausta...
|
.opening-the-command-reference = Avataan komentoviittausta...
|
||||||
|
.image-refusal-unknown-size = Kohteen "__ocs_fmt_0__" kokoa ei voitu mitata otsakkeesta ja sen purkaminen ylittäisi tämän käännöksen __ocs_fmt_1__ MB kuvarajan — sitä ei voi liittää.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" on jo ladattu eri sisällöllä; olemassa oleva tiedosto säilytettiin.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" on jo ladattu eri sisällöllä; olemassa oleva tiedosto säilytettiin.
|
||||||
.opening-patreon-page = Avataan Patreon-sivua...
|
.opening-patreon-page = Avataan Patreon-sivua...
|
||||||
.opening-feedback-page = Palautesivua avataan...
|
.opening-feedback-page = Palautesivua avataan...
|
||||||
|
|||||||
@@ -3577,6 +3577,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Ouverture OCS Discussions pour l'aide et les questions...
|
.opening-ocs-discussions-for-help-and-questions = Ouverture OCS Discussions pour l'aide et les questions...
|
||||||
.opening-ocs-web = Ouverture du site Web OCS...
|
.opening-ocs-web = Ouverture du site Web OCS...
|
||||||
.opening-the-command-reference = Ouverture de la référence des commandes...
|
.opening-the-command-reference = Ouverture de la référence des commandes...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" n’a pas pu être mesuré par son en-tête et son décodage dépasserait la limite de __ocs_fmt_1__ Mo de cette version — impossible de l’attacher.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" est déjà chargé avec un contenu différent ; le fichier existant a été conservé.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" est déjà chargé avec un contenu différent ; le fichier existant a été conservé.
|
||||||
.opening-patreon-page = Ouverture de la page Patreon...
|
.opening-patreon-page = Ouverture de la page Patreon...
|
||||||
.opening-feedback-page = Ouverture de la page de rétroaction...
|
.opening-feedback-page = Ouverture de la page de rétroaction...
|
||||||
|
|||||||
@@ -3565,6 +3565,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = OCS मदद और प्रश्नों के लिए चर्चा खोलना।
|
.opening-ocs-discussions-for-help-and-questions = OCS मदद और प्रश्नों के लिए चर्चा खोलना।
|
||||||
.opening-ocs-web = OCS वेब खोलना ...
|
.opening-ocs-web = OCS वेब खोलना ...
|
||||||
.opening-the-command-reference = कमांड संदर्भ खोला जा रहा है...
|
.opening-the-command-reference = कमांड संदर्भ खोला जा रहा है...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" को उसके हेडर से मापा नहीं जा सका और उसे डिकोड करना इस बिल्ड की __ocs_fmt_1__ MB छवि सीमा पार कर देगा — जोड़ा नहीं जा सकता।
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" अलग सामग्री के साथ पहले से लोड है; मौजूदा फ़ाइल रखी गई।
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" अलग सामग्री के साथ पहले से लोड है; मौजूदा फ़ाइल रखी गई।
|
||||||
.opening-patreon-page = Patreon पृष्ठ खोला जा रहा है…
|
.opening-patreon-page = Patreon पृष्ठ खोला जा रहा है…
|
||||||
.opening-feedback-page = प्रतिक्रिया पृष्ठ खोलना
|
.opening-feedback-page = प्रतिक्रिया पृष्ठ खोलना
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = A OCS beszélgetések megnyitása segítségért és kérdésekért...
|
.opening-ocs-discussions-for-help-and-questions = A OCS beszélgetések megnyitása segítségért és kérdésekért...
|
||||||
.opening-ocs-web = A OCS web megnyitása...
|
.opening-ocs-web = A OCS web megnyitása...
|
||||||
.opening-the-command-reference = A parancsreferencia megnyitása...
|
.opening-the-command-reference = A parancsreferencia megnyitása...
|
||||||
|
.image-refusal-unknown-size = A(z) "__ocs_fmt_0__" nem mérhető a fejlécéből, és dekódolása meghaladná ezen build __ocs_fmt_1__ MB-os képhatárát — nem csatolható.
|
||||||
.already-loaded-different-contents-kept = A(z) "__ocs_fmt_0__" már betöltve eltérő tartalommal; a meglévő fájl megőrizve.
|
.already-loaded-different-contents-kept = A(z) "__ocs_fmt_0__" már betöltve eltérő tartalommal; a meglévő fájl megőrizve.
|
||||||
.opening-patreon-page = Patreon oldal megnyitása...
|
.opening-patreon-page = Patreon oldal megnyitása...
|
||||||
.opening-feedback-page = Visszajelzési oldal megnyitása...
|
.opening-feedback-page = Visszajelzési oldal megnyitása...
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Apertura delle discussioni OCS per assistenza e domande...
|
.opening-ocs-discussions-for-help-and-questions = Apertura delle discussioni OCS per assistenza e domande...
|
||||||
.opening-ocs-web = Apertura del Web OCS...
|
.opening-ocs-web = Apertura del Web OCS...
|
||||||
.opening-the-command-reference = Apertura del riferimento dei comandi...
|
.opening-the-command-reference = Apertura del riferimento dei comandi...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" non è stato misurabile dall’intestazione e decodificarlo supererebbe il limite di __ocs_fmt_1__ MB di questa build — impossibile allegarlo.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" è già caricato con contenuti diversi; il file esistente è stato mantenuto.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" è già caricato con contenuti diversi; il file esistente è stato mantenuto.
|
||||||
.opening-patreon-page = Apertura della pagina Patreon...
|
.opening-patreon-page = Apertura della pagina Patreon...
|
||||||
.opening-feedback-page = Apertura della pagina di feedback...
|
.opening-feedback-page = Apertura della pagina di feedback...
|
||||||
|
|||||||
@@ -3565,6 +3565,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = ヘルプと質問のため OCS ディスカッションを開いています...
|
.opening-ocs-discussions-for-help-and-questions = ヘルプと質問のため OCS ディスカッションを開いています...
|
||||||
.opening-ocs-web = OCS Web を開いています...
|
.opening-ocs-web = OCS Web を開いています...
|
||||||
.opening-the-command-reference = コマンドリファレンスを開いています...
|
.opening-the-command-reference = コマンドリファレンスを開いています...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" のサイズをヘッダーから測定できず、デコードするとこのビルドの __ocs_fmt_1__ MB 画像制限を超えます — 添付できません。
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" は既に別の内容で読み込まれています。既存のファイルを保持しました。
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" は既に別の内容で読み込まれています。既存のファイルを保持しました。
|
||||||
.opening-patreon-page = Patreon ページを開いています...
|
.opening-patreon-page = Patreon ページを開いています...
|
||||||
.opening-feedback-page = フィードバックページを開いています...
|
.opening-feedback-page = フィードバックページを開いています...
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = OCS 열기 도움말 및 질문에 대한 토론...
|
.opening-ocs-discussions-for-help-and-questions = OCS 열기 도움말 및 질문에 대한 토론...
|
||||||
.opening-ocs-web = OCS 웹을 여는 중...
|
.opening-ocs-web = OCS 웹을 여는 중...
|
||||||
.opening-the-command-reference = 명령 참조 열기...
|
.opening-the-command-reference = 명령 참조 열기...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__"의 크기를 헤더에서 측정할 수 없고 디코딩 시 이 빌드의 __ocs_fmt_1__ MB 이미지 한도를 초과합니다 — 첨부할 수 없습니다.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__"이(가) 이미 다른 내용으로 로드되었습니다. 기존 파일을 유지했습니다.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__"이(가) 이미 다른 내용으로 로드되었습니다. 기존 파일을 유지했습니다.
|
||||||
.opening-patreon-page = Patreon 페이지를 여는 중...
|
.opening-patreon-page = Patreon 페이지를 여는 중...
|
||||||
.opening-feedback-page = 피드백 페이지를 여는 중...
|
.opening-feedback-page = 피드백 페이지를 여는 중...
|
||||||
|
|||||||
@@ -3575,6 +3575,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Openen OCS Discussie voor hulp en vragen...
|
.opening-ocs-discussions-for-help-and-questions = Openen OCS Discussie voor hulp en vragen...
|
||||||
.opening-ocs-web = Openen OCS Web...
|
.opening-ocs-web = Openen OCS Web...
|
||||||
.opening-the-command-reference = Commandoreferentie openen...
|
.opening-the-command-reference = Commandoreferentie openen...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" kon niet via de header gemeten worden en decoderen overschrijdt de __ocs_fmt_1__ MB-limiet van deze build — kan niet worden toegevoegd.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" is al geladen met andere inhoud; het bestaande bestand is bewaard.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" is al geladen met andere inhoud; het bestaande bestand is bewaard.
|
||||||
.opening-patreon-page = Patreon pagina openen...
|
.opening-patreon-page = Patreon pagina openen...
|
||||||
.opening-feedback-page = Herinneringspagina openen...
|
.opening-feedback-page = Herinneringspagina openen...
|
||||||
|
|||||||
@@ -3576,6 +3576,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Otwieranie dyskusji OCS w celu uzyskania pomocy i pytań...
|
.opening-ocs-discussions-for-help-and-questions = Otwieranie dyskusji OCS w celu uzyskania pomocy i pytań...
|
||||||
.opening-ocs-web = Otwieranie OCS Web...
|
.opening-ocs-web = Otwieranie OCS Web...
|
||||||
.opening-the-command-reference = Otwieranie dokumentacji poleceń...
|
.opening-the-command-reference = Otwieranie dokumentacji poleceń...
|
||||||
|
.image-refusal-unknown-size = „__ocs_fmt_0__” nie dał się zmierzyć z nagłówka, a jego dekodowanie przekroczy limit __ocs_fmt_1__ MB tej kompilacji — nie można go dołączyć.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" jest już wczytany z inną zawartością; zachowano istniejący plik.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" jest już wczytany z inną zawartością; zachowano istniejący plik.
|
||||||
.opening-patreon-page = Otwieranie strony Patreon...
|
.opening-patreon-page = Otwieranie strony Patreon...
|
||||||
.opening-feedback-page = Otwieram stronę z opiniami...
|
.opening-feedback-page = Otwieram stronę z opiniami...
|
||||||
|
|||||||
@@ -3577,6 +3577,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Abrindo Discussões do OCS para ajuda e perguntas...
|
.opening-ocs-discussions-for-help-and-questions = Abrindo Discussões do OCS para ajuda e perguntas...
|
||||||
.opening-ocs-web = Abrindo OCS Web...
|
.opening-ocs-web = Abrindo OCS Web...
|
||||||
.opening-the-command-reference = Abrindo a referência de comandos...
|
.opening-the-command-reference = Abrindo a referência de comandos...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" não pôde ser medido pelo cabeçalho e a sua descodificação excedeu o limite de __ocs_fmt_1__ MB desta build — não pode ser anexado.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" já está carregado com conteúdo diferente; o ficheiro existente foi mantido.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" já está carregado com conteúdo diferente; o ficheiro existente foi mantido.
|
||||||
.opening-patreon-page = Abrindo página do Patreon...
|
.opening-patreon-page = Abrindo página do Patreon...
|
||||||
.opening-feedback-page = Abrindo página de comentários...
|
.opening-feedback-page = Abrindo página de comentários...
|
||||||
|
|||||||
@@ -3578,6 +3578,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Открытие OCS Дискуссии о помощи и вопросах
|
.opening-ocs-discussions-for-help-and-questions = Открытие OCS Дискуссии о помощи и вопросах
|
||||||
.opening-ocs-web = Открытие OCS Веб-сайт
|
.opening-ocs-web = Открытие OCS Веб-сайт
|
||||||
.opening-the-command-reference = Открытие справочника команд...
|
.opening-the-command-reference = Открытие справочника команд...
|
||||||
|
.image-refusal-unknown-size = «__ocs_fmt_0__» не удалось измерить по заголовку, а его декодирование превысит лимит __ocs_fmt_1__ МБ этой сборки — вложить нельзя.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" уже загружен с другим содержимым; существующий файл сохранён.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" уже загружен с другим содержимым; существующий файл сохранён.
|
||||||
.opening-patreon-page = Открытие страницы Patreon...
|
.opening-patreon-page = Открытие страницы Patreon...
|
||||||
.opening-feedback-page = Открываем страницу обратной связи...
|
.opening-feedback-page = Открываем страницу обратной связи...
|
||||||
|
|||||||
@@ -3527,6 +3527,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = Yardım ve sorular için OCS Tartışmaları açılıyor...
|
.opening-ocs-discussions-for-help-and-questions = Yardım ve sorular için OCS Tartışmaları açılıyor...
|
||||||
.opening-ocs-web = OCS Web açılıyor...
|
.opening-ocs-web = OCS Web açılıyor...
|
||||||
.opening-the-command-reference = Komut başvurusu açılıyor...
|
.opening-the-command-reference = Komut başvurusu açılıyor...
|
||||||
|
.image-refusal-unknown-size = "__ocs_fmt_0__" başlığından ölçülemedi ve kodu çözülürse bu sürümün __ocs_fmt_1__ MB görüntü limiti aşılır — eklenemez.
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__" zaten farklı içerikle yüklü; mevcut dosya tutuldu.
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__" zaten farklı içerikle yüklü; mevcut dosya tutuldu.
|
||||||
.opening-patreon-page = Patreon sayfası açılıyor...
|
.opening-patreon-page = Patreon sayfası açılıyor...
|
||||||
.opening-feedback-page = Geri bildirim sayfası açılıyor...
|
.opening-feedback-page = Geri bildirim sayfası açılıyor...
|
||||||
|
|||||||
@@ -3560,6 +3560,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = 开幕OCS 讨论帮助和问题...
|
.opening-ocs-discussions-for-help-and-questions = 开幕OCS 讨论帮助和问题...
|
||||||
.opening-ocs-web = 打开OCS网络...
|
.opening-ocs-web = 打开OCS网络...
|
||||||
.opening-the-command-reference = 正在打开命令参考...
|
.opening-the-command-reference = 正在打开命令参考...
|
||||||
|
.image-refusal-unknown-size = 无法从文件头测得“__ocs_fmt_0__”的尺寸,解码将超过本版本 __ocs_fmt_1__ MB 的图像上限——无法附着。
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同内容加载;保留了现有文件。
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同内容加载;保留了现有文件。
|
||||||
.opening-patreon-page = 打开帕特伦的页面...
|
.opening-patreon-page = 打开帕特伦的页面...
|
||||||
.opening-feedback-page = 打开反馈页面...
|
.opening-feedback-page = 打开反馈页面...
|
||||||
|
|||||||
@@ -3572,6 +3572,7 @@ view =
|
|||||||
.opening-ocs-discussions-for-help-and-questions = 開幕OCS 討論幫助和問題...
|
.opening-ocs-discussions-for-help-and-questions = 開幕OCS 討論幫助和問題...
|
||||||
.opening-ocs-web = 開啟OCS網路...
|
.opening-ocs-web = 開啟OCS網路...
|
||||||
.opening-the-command-reference = 正在開啟命令參考...
|
.opening-the-command-reference = 正在開啟命令參考...
|
||||||
|
.image-refusal-unknown-size = 無法從檔頭測得「__ocs_fmt_0__」的尺寸,解碼將超過本版本 __ocs_fmt_1__ MB 的影像上限——無法附加。
|
||||||
.already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同內容載入;保留了現有檔案。
|
.already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同內容載入;保留了現有檔案。
|
||||||
.opening-patreon-page = 開啟帕特倫的頁面...
|
.opening-patreon-page = 開啟帕特倫的頁面...
|
||||||
.opening-feedback-page = 開啟反饋頁面...
|
.opening-feedback-page = 開啟反饋頁面...
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ trunk build --release --dist dist/app --public-url /app/ "$@"
|
|||||||
|
|
||||||
# Mirror the font sources over the bundle and verify the copy-dir output.
|
# Mirror the font sources over the bundle and verify the copy-dir output.
|
||||||
mkdir -p dist/app/fonts
|
mkdir -p dist/app/fonts
|
||||||
|
# Hard-fail with a clear message when there are no fonts to verify.
|
||||||
|
first_font=$(echo web/fonts/*.ttf)
|
||||||
|
if [ ! -e "$first_font" ]; then
|
||||||
|
echo "build-web.sh: no .ttf fonts found under web/fonts — nothing to ship or verify" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
cp -f web/fonts/* dist/app/fonts/
|
cp -f web/fonts/* dist/app/fonts/
|
||||||
|
|
||||||
count=0
|
count=0
|
||||||
|
|||||||
@@ -448,6 +448,14 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
registered.extend(fetched);
|
registered.extend(fetched);
|
||||||
|
if crate::scene::model::image_model::take_overflowed() {
|
||||||
|
// A settled fetch's rebuild trigger was dropped at the
|
||||||
|
// outcome cap; do one unconditional catch-up pass over
|
||||||
|
// every registered name.
|
||||||
|
registered.extend(
|
||||||
|
crate::scene::model::image_model::registered_image_names(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if !registered.is_empty() {
|
if !registered.is_empty() {
|
||||||
for tab in self.tabs.iter_mut() {
|
for tab in self.tabs.iter_mut() {
|
||||||
tab.scene.resolve_registered_image_names(®istered);
|
tab.scene.resolve_registered_image_names(®istered);
|
||||||
|
|||||||
+1
-1
@@ -1905,7 +1905,7 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
|||||||
// references, which must survive for their own drains.
|
// references, which must survive for their own drains.
|
||||||
crate::scene::model::image_model::decode_refusals()
|
crate::scene::model::image_model::decode_refusals()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|note| note.contains(short.as_str()))
|
.find(|note| note.contains(&format!("\"{}\"", short)))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| format!("Unsupported or corrupt image \"{short}\""))
|
.unwrap_or_else(|| format!("Unsupported or corrupt image \"{short}\""))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1886,6 +1886,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Opening OCS Discussions for help and questions..." => Some(("view", "opening-ocs-discussions-for-help-and-questions")),
|
"Opening OCS Discussions for help and questions..." => Some(("view", "opening-ocs-discussions-for-help-and-questions")),
|
||||||
"Opening OCS Web..." => Some(("view", "opening-ocs-web")),
|
"Opening OCS Web..." => Some(("view", "opening-ocs-web")),
|
||||||
"Opening the command reference..." => Some(("view", "opening-the-command-reference")),
|
"Opening the command reference..." => Some(("view", "opening-the-command-reference")),
|
||||||
|
"\"{name}\" could not be sized from its header and decoding it exceeded the {limit} MB image limit of this build — it cannot be attached." => Some(("file", "image-refusal-unknown-size")),
|
||||||
"\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("view", "already-loaded-different-contents-kept")),
|
"\"{name}\" is already loaded with different contents; the existing file was kept." => Some(("view", "already-loaded-different-contents-kept")),
|
||||||
"Opening Patreon page..." => Some(("view", "opening-patreon-page")),
|
"Opening Patreon page..." => Some(("view", "opening-patreon-page")),
|
||||||
"Opening feedback page..." => Some(("view", "opening-feedback-page")),
|
"Opening feedback page..." => Some(("view", "opening-feedback-page")),
|
||||||
|
|||||||
+245
-49
@@ -135,6 +135,19 @@ impl ImageModel {
|
|||||||
pub fn from_raster_image(img: &acadrust::entities::RasterImage) -> Option<Self> {
|
pub fn from_raster_image(img: &acadrust::entities::RasterImage) -> Option<Self> {
|
||||||
let w = img.size.x;
|
let w = img.size.x;
|
||||||
let h = img.size.y;
|
let h = img.size.y;
|
||||||
|
// A degenerate or non-finite size/vectors (hostile entity data)
|
||||||
|
// produce NaN placement math that aborts whole PDF exports; treat
|
||||||
|
// it as an unresolvable reference instead.
|
||||||
|
if !(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let vector_ok = |v: &acadrust::Vector3| {
|
||||||
|
let (x, y, z) = (v.x, v.y, v.z);
|
||||||
|
x.is_finite() && y.is_finite() && z.is_finite() && (x != 0.0 || y != 0.0 || z != 0.0)
|
||||||
|
};
|
||||||
|
if !vector_ok(&img.u_vector) || !vector_ok(&img.v_vector) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
// Model-space geometry is drawn in (WCS - world_offset) so large UTM-
|
// Model-space geometry is drawn in (WCS - world_offset) so large UTM-
|
||||||
// scale coordinates stay within f32 precision; offset the image too.
|
// scale coordinates stay within f32 precision; offset the image too.
|
||||||
// Corners come from a large insertion point plus small u/v spans.
|
// Corners come from a large insertion point plus small u/v spans.
|
||||||
@@ -396,6 +409,14 @@ fn image_cache() -> &'static Mutex<HashMap<String, ImageCacheEntry>> {
|
|||||||
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insertion order of the source cache's keys, so by-name resolution has a
|
||||||
|
/// deterministic winner (the MOST RECENT registration) instead of HashMap's
|
||||||
|
/// unspecified iteration order.
|
||||||
|
fn source_order() -> &'static Mutex<Vec<String>> {
|
||||||
|
static ORDER: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
|
||||||
|
ORDER.get_or_init(|| Mutex::new(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
fn source_cache() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
fn source_cache() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
||||||
static SOURCES: OnceLock<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceLock::new();
|
static SOURCES: OnceLock<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceLock::new();
|
||||||
SOURCES.get_or_init(|| Mutex::new(HashMap::new()))
|
SOURCES.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
@@ -428,10 +449,18 @@ pub(crate) fn same_file_name(a: &str, b: &str) -> bool {
|
|||||||
/// finally supplies a missing reference) re-decodes instead of replaying a
|
/// finally supplies a missing reference) re-decodes instead of replaying a
|
||||||
/// stale success or a cached failure.
|
/// stale success or a cached failure.
|
||||||
pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
||||||
source_cache()
|
{
|
||||||
.lock()
|
let mut sources = source_cache()
|
||||||
.unwrap_or_else(|error| error.into_inner())
|
.lock()
|
||||||
.insert(path.to_string(), bytes);
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
|
if !sources.contains_key(path) {
|
||||||
|
source_order()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
|
.push(path.to_string());
|
||||||
|
}
|
||||||
|
sources.insert(path.to_string(), bytes);
|
||||||
|
}
|
||||||
if let Ok(mut cache) = image_cache().lock() {
|
if let Ok(mut cache) = image_cache().lock() {
|
||||||
cache.retain(|cached, _| !same_file_name(cached, path));
|
cache.retain(|cached, _| !same_file_name(cached, path));
|
||||||
}
|
}
|
||||||
@@ -466,10 +495,17 @@ fn registered_bytes_by_name(path: &str) -> Option<Arc<Vec<u8>>> {
|
|||||||
let cache = source_cache()
|
let cache = source_cache()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|error| error.into_inner());
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
cache
|
// Deterministic winner: walk the insertion order newest-first so the
|
||||||
|
// most recent drop/pick wins, instead of HashMap iteration order.
|
||||||
|
let order = source_order()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
|
order
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(key, _)| same_file_name(key, path))
|
.rev()
|
||||||
.map(|(_, bytes)| Arc::clone(bytes))
|
.filter(|key| cache.contains_key(*key))
|
||||||
|
.find(|key| same_file_name(key, path))
|
||||||
|
.and_then(|key| cache.get(key).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Original image file bytes registered for a reference, resolved the way
|
/// Original image file bytes registered for a reference, resolved the way
|
||||||
@@ -593,12 +629,51 @@ enum DecodeTier {
|
|||||||
Full,
|
Full,
|
||||||
/// Decode fully, then downsample to the display cap before storing.
|
/// Decode fully, then downsample to the display cap before storing.
|
||||||
Downsample,
|
Downsample,
|
||||||
/// Full RGBA would exceed `MAX_DECODE_BYTES`. The decode still runs with
|
/// The decode's peak memory would exceed `MAX_DECODE_BYTES`. The probe
|
||||||
/// the budget enforced through `image::Limits`: it succeeds on a build
|
/// bills the peak of the decoder's NATIVE bytes per pixel and the final
|
||||||
/// with headroom (native) or errors into a refusal (wasm).
|
/// RGBA buffer (4/px): both transiently coexist mid-decode on 16-bit
|
||||||
|
/// formats, and the decoder's own limits reserve at native bpp — so an
|
||||||
|
/// image is refused up front when either exceeds the budget, without
|
||||||
|
/// decoding.
|
||||||
OverBudget,
|
OverBudget,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The decoder's own memory accounting for a format: how many bytes per
|
||||||
|
/// pixel the DECODE transiently needs (native layout, plus the RGBA output
|
||||||
|
/// where they coexist) and any codec-specific scratch multiplier on top.
|
||||||
|
struct NativeFootprint {
|
||||||
|
/// Peak bytes per pixel during decode (native bpp + RGBA where both
|
||||||
|
/// live simultaneously; plain 8-bit formats overlap them in place).
|
||||||
|
peak_bpp: u64,
|
||||||
|
/// Extra codec scratch allowance the decoder stacks on the reserve
|
||||||
|
/// (image's TIFF codec needs ~3x in the worst strip/tile layout).
|
||||||
|
codec_factor: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_footprint(format: Option<image::ImageFormat>, color: Option<image::ColorType>) -> NativeFootprint {
|
||||||
|
let native_bpp = color.map_or(4u64, |color| match color {
|
||||||
|
image::ColorType::L8 => 1,
|
||||||
|
image::ColorType::La8 => 2,
|
||||||
|
image::ColorType::Rgb8 => 3,
|
||||||
|
image::ColorType::Rgba8 => 4,
|
||||||
|
image::ColorType::L16 => 2,
|
||||||
|
image::ColorType::La16 => 4,
|
||||||
|
image::ColorType::Rgb16 => 6,
|
||||||
|
image::ColorType::Rgba16 => 8,
|
||||||
|
image::ColorType::Rgb32F => 12,
|
||||||
|
image::ColorType::Rgba32F => 16,
|
||||||
|
_ => 4,
|
||||||
|
});
|
||||||
|
// A 16-bit frame and its RGBA conversion can coexist; an 8-bit one is
|
||||||
|
// converted with at most a small row buffer in flight.
|
||||||
|
let peak_bpp = if native_bpp > 4 { native_bpp + 4 } else { 4 };
|
||||||
|
let codec_factor = match format {
|
||||||
|
Some(image::ImageFormat::Tiff) => 3,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
NativeFootprint { peak_bpp, codec_factor }
|
||||||
|
}
|
||||||
|
|
||||||
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
|
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
|
||||||
/// when the budgets are passed explicitly, so tests can probe arbitrary
|
/// when the budgets are passed explicitly, so tests can probe arbitrary
|
||||||
/// budgets; the decode path always passes this target's constants.
|
/// budgets; the decode path always passes this target's constants.
|
||||||
@@ -607,9 +682,27 @@ fn tier_for(
|
|||||||
height: u32,
|
height: u32,
|
||||||
max_decode_bytes: u64,
|
max_decode_bytes: u64,
|
||||||
max_display_pixels: u64,
|
max_display_pixels: u64,
|
||||||
|
) -> DecodeTier {
|
||||||
|
tier_for_native(width, height, 4, 1, max_decode_bytes, max_display_pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`tier_for`] with the probed format's native footprint: the caller
|
||||||
|
/// supplies the probe's color type and format so 16-bit images are billed
|
||||||
|
/// at what their decode actually allocates.
|
||||||
|
fn tier_for_native(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
peak_bpp: u64,
|
||||||
|
codec_factor: u64,
|
||||||
|
max_decode_bytes: u64,
|
||||||
|
max_display_pixels: u64,
|
||||||
) -> DecodeTier {
|
) -> DecodeTier {
|
||||||
let pixels = width as u64 * height as u64;
|
let pixels = width as u64 * height as u64;
|
||||||
if pixels.saturating_mul(4) > max_decode_bytes {
|
if pixels
|
||||||
|
.saturating_mul(peak_bpp)
|
||||||
|
.saturating_mul(codec_factor)
|
||||||
|
> max_decode_bytes
|
||||||
|
{
|
||||||
DecodeTier::OverBudget
|
DecodeTier::OverBudget
|
||||||
} else if pixels > max_display_pixels {
|
} else if pixels > max_display_pixels {
|
||||||
DecodeTier::Downsample
|
DecodeTier::Downsample
|
||||||
@@ -658,24 +751,45 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
|||||||
max_decode_bytes: u64,
|
max_decode_bytes: u64,
|
||||||
max_display_pixels: u64,
|
max_display_pixels: u64,
|
||||||
) -> DecodeAttempt {
|
) -> DecodeAttempt {
|
||||||
// Probe pass: header dimensions only, no pixel decode.
|
// Probe pass: header dimensions AND color type, no pixel decode. The
|
||||||
|
// probe reader carries the full budget too so constructing a decoder on
|
||||||
|
// a huge-but-in-budget header cannot itself fail on image's default
|
||||||
|
// 512 MiB probe limits.
|
||||||
let probed = open()
|
let probed = open()
|
||||||
.and_then(|reader| reader.with_guessed_format())
|
.and_then(|reader| reader.with_guessed_format())
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|reader| reader.into_dimensions().ok());
|
.and_then(|mut reader| {
|
||||||
let tier = probed.map_or(DecodeTier::Full, |(width, height)| {
|
let format = reader.format();
|
||||||
tier_for(width, height, max_decode_bytes, max_display_pixels)
|
let mut probe_limits = image::Limits::default();
|
||||||
|
probe_limits.max_alloc = Some(max_decode_bytes);
|
||||||
|
reader.limits(probe_limits);
|
||||||
|
use image::ImageDecoder as _;
|
||||||
|
let decoder = reader.into_decoder().ok()?;
|
||||||
|
let (width, height) = decoder.dimensions();
|
||||||
|
let color = decoder.color_type();
|
||||||
|
Some((width, height, format, color))
|
||||||
|
});
|
||||||
|
let tier = probed.map_or(DecodeTier::Full, |(width, height, format, color)| {
|
||||||
|
let footprint = native_footprint(format, Some(color));
|
||||||
|
tier_for_native(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
footprint.peak_bpp,
|
||||||
|
footprint.codec_factor,
|
||||||
|
max_decode_bytes,
|
||||||
|
max_display_pixels,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trust the probe, not the decoder limits: `Limits::reserve` checks the
|
// Trust the probe, not the decoder limits: the tier above bills the
|
||||||
// decoder's NATIVE bytes-per-pixel, so a grayscale/RGB JPEG/PNG/BMP over
|
// decode's real peak (native bpp, codec scratch), so an OverBudget
|
||||||
// the RGBA budget decodes fine and then `into_rgba8()` balloons to up to
|
// image provably cannot fit; refuse without decoding. This covers the
|
||||||
// 4x it — exactly the tab death this exists to prevent. The probed
|
// grayscale/RGB over-RGBA-budget balloon AND the 16-bit formats whose
|
||||||
// dimensions already prove it cannot fit; refuse without decoding.
|
// native layout is double the RGBA size.
|
||||||
if tier == DecodeTier::OverBudget {
|
if tier == DecodeTier::OverBudget {
|
||||||
return DecodeAttempt::Refused {
|
return DecodeAttempt::Refused {
|
||||||
width: probed.map_or(0, |(width, _)| width),
|
width: probed.map_or(0, |(width, _, _, _)| width),
|
||||||
height: probed.map_or(0, |(_, height)| height),
|
height: probed.map_or(0, |(_, height, _, _)| height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,24 +798,32 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
|||||||
Err(_) => return DecodeAttempt::Failed,
|
Err(_) => return DecodeAttempt::Failed,
|
||||||
};
|
};
|
||||||
// For formats that FIT the budget, the limits still guard runaway
|
// For formats that FIT the budget, the limits still guard runaway
|
||||||
// allocations mid-decode. TIFF additionally stacks its own intermediate
|
// allocations mid-decode. GIF is special: the probe reads the LOGICAL
|
||||||
// scratch buffers on top of the reserve (empirically ~3x the frame size
|
// SCREEN, but a crafted frame may declare itself arbitrarily larger
|
||||||
// in image 0.25), so refusing an in-budget TIFF at face value would
|
// and the gif crate does not enforce frame-vs-screen consistency — so
|
||||||
// produce a note claiming e.g. "400 MB, over the 512 MB limit".
|
// the frame buffer is capped to the probed size plus slack, refusing
|
||||||
let tiff = matches!(reader.format(), Some(image::ImageFormat::Tiff));
|
// frame bombs into the existing Refused path. TIFF stacks its own
|
||||||
|
// intermediate scratch on top of the reserve (~3x worst case in image
|
||||||
|
// 0.25); the tier already folded that factor in, and the allowance
|
||||||
|
// matches it so an in-tier TIFF never codec-refuses.
|
||||||
let mut limits = image::Limits::default();
|
let mut limits = image::Limits::default();
|
||||||
limits.max_alloc = Some(if tiff {
|
limits.max_alloc = Some(match reader.format() {
|
||||||
max_decode_bytes.saturating_mul(3)
|
Some(image::ImageFormat::Gif) => probed
|
||||||
} else {
|
.map(|(width, height, _, _)| {
|
||||||
max_decode_bytes
|
(width as u64).saturating_mul(height as u64).saturating_mul(4) + 64 * 1024 * 1024
|
||||||
|
})
|
||||||
|
.unwrap_or(max_decode_bytes)
|
||||||
|
.min(max_decode_bytes),
|
||||||
|
Some(image::ImageFormat::Tiff) => max_decode_bytes.saturating_mul(3),
|
||||||
|
_ => max_decode_bytes,
|
||||||
});
|
});
|
||||||
reader.limits(limits);
|
reader.limits(limits);
|
||||||
let img = match reader.decode() {
|
let img = match reader.decode() {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(image::ImageError::Limits(_)) => {
|
Err(image::ImageError::Limits(_)) => {
|
||||||
return DecodeAttempt::Refused {
|
return DecodeAttempt::Refused {
|
||||||
width: probed.map_or(0, |(width, _)| width),
|
width: probed.map_or(0, |(width, _, _, _)| width),
|
||||||
height: probed.map_or(0, |(_, height)| height),
|
height: probed.map_or(0, |(_, height, _, _)| height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Err(_) => return DecodeAttempt::Failed,
|
Err(_) => return DecodeAttempt::Failed,
|
||||||
@@ -726,7 +848,7 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
|||||||
let (stored_w, stored_h) = rgba.dimensions();
|
let (stored_w, stored_h) = rgba.dimensions();
|
||||||
// Sizing truth: the file's probed dimensions, not the (possibly reduced)
|
// Sizing truth: the file's probed dimensions, not the (possibly reduced)
|
||||||
// buffer's, so placement and aspect come from the original raster.
|
// buffer's, so placement and aspect come from the original raster.
|
||||||
let (source_width, source_height) = probed.unwrap_or((stored_w, stored_h));
|
let (source_width, source_height) = probed.map_or((stored_w, stored_h), |(w, h, _, _)| (w, h));
|
||||||
DecodeAttempt::Decoded(DecodedImage {
|
DecodeAttempt::Decoded(DecodedImage {
|
||||||
pixels: Arc::new(rgba.into_raw()),
|
pixels: Arc::new(rgba.into_raw()),
|
||||||
width: stored_w,
|
width: stored_w,
|
||||||
@@ -765,6 +887,16 @@ fn refusal_message(path: &str, width: u32, height: u32) -> String {
|
|||||||
.file_name()
|
.file_name()
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| path.to_string());
|
.unwrap_or_else(|| path.to_string());
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
// The probe could not determine a size (exotic header) — do not
|
||||||
|
// print a self-contradictory "0x0 px" figure.
|
||||||
|
return crate::tf!(
|
||||||
|
"\"{name}\" could not be sized from its header and decoding it exceeded the {limit} MB image limit of this build — it cannot be attached.",
|
||||||
|
name = name,
|
||||||
|
limit = MAX_DECODE_BYTES / (1024 * 1024)
|
||||||
|
)
|
||||||
|
.into_owned();
|
||||||
|
}
|
||||||
let decoded_mb = (width as u64).saturating_mul(height as u64).saturating_mul(4) / (1024 * 1024);
|
let decoded_mb = (width as u64).saturating_mul(height as u64).saturating_mul(4) / (1024 * 1024);
|
||||||
let limit_mb = MAX_DECODE_BYTES / (1024 * 1024);
|
let limit_mb = MAX_DECODE_BYTES / (1024 * 1024);
|
||||||
crate::tf!(
|
crate::tf!(
|
||||||
@@ -921,11 +1053,32 @@ mod web_fetch {
|
|||||||
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
|
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
|
||||||
"ocs: remote-image outcome queue full; dropping an outcome",
|
"ocs: remote-image outcome queue full; dropping an outcome",
|
||||||
));
|
));
|
||||||
|
// The dropped outcome may be a Loaded whose rebuild trigger is
|
||||||
|
// lost; remember that so the next poll does one unconditional
|
||||||
|
// image re-resolution instead of waiting for another event.
|
||||||
|
*overflowed()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner()) = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
queue.push_back(outcome);
|
queue.push_back(outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether an outcome was dropped at the cap and a catch-up rebuild is
|
||||||
|
/// owed. [`take_overflowed`] consumes it.
|
||||||
|
fn overflowed() -> &'static Mutex<bool> {
|
||||||
|
static FLAG: OnceLock<Mutex<bool>> = OnceLock::new();
|
||||||
|
FLAG.get_or_init(|| Mutex::new(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume the dropped-outcome flag (see [`overflowed`]).
|
||||||
|
pub fn take_overflowed() -> bool {
|
||||||
|
let mut flag = overflowed()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
std::mem::replace(&mut *flag, false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Pop the oldest settled remote fetch the poll has not handled yet.
|
/// Pop the oldest settled remote fetch the poll has not handled yet.
|
||||||
pub fn take_outcome() -> Option<RemoteFetchOutcome> {
|
pub fn take_outcome() -> Option<RemoteFetchOutcome> {
|
||||||
outcomes()
|
outcomes()
|
||||||
@@ -946,6 +1099,58 @@ mod web_fetch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a response body incrementally, refusing past [`MAX_BYTES`] the
|
||||||
|
/// moment the cap is crossed (not after buffering the whole body).
|
||||||
|
async fn stream_body_capped(
|
||||||
|
window: &web_sys::Window,
|
||||||
|
response: &web_sys::Response,
|
||||||
|
) -> Result<Vec<u8>, RemoteFetchError> {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
|
||||||
|
let Some(stream) = response.body() else {
|
||||||
|
// No stream API (or already consumed): fall back to buffering,
|
||||||
|
// with the received-length check still applying after.
|
||||||
|
let buffer = JsFuture::from(
|
||||||
|
response.array_buffer().map_err(|_| RemoteFetchError::Network)?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| rejection_kind(&error))?;
|
||||||
|
let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
|
||||||
|
if bytes.len() as u64 > MAX_BYTES {
|
||||||
|
return Err(RemoteFetchError::TooLarge);
|
||||||
|
}
|
||||||
|
return Ok(bytes);
|
||||||
|
};
|
||||||
|
let reader = stream.get_reader().dyn_into::<web_sys::ReadableStreamDefaultReader>()
|
||||||
|
.map_err(|_| RemoteFetchError::Network)?;
|
||||||
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
|
loop {
|
||||||
|
let chunk = JsFuture::from(reader.read())
|
||||||
|
.await
|
||||||
|
.map_err(|error| rejection_kind(&error))?;
|
||||||
|
let done = js_sys::Reflect::get(&chunk, &wasm_bindgen::JsValue::from_str("done"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_bool())
|
||||||
|
.unwrap_or(true);
|
||||||
|
if done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let value = js_sys::Reflect::get(&chunk, &wasm_bindgen::JsValue::from_str("value"))
|
||||||
|
.map_err(|_| RemoteFetchError::Network)?;
|
||||||
|
let view = js_sys::Uint8Array::new(&value);
|
||||||
|
if bytes.len() as u64 + view.length() as u64 > MAX_BYTES {
|
||||||
|
// Release the reader lock so the underlying connection can
|
||||||
|
// be torn down, then refuse.
|
||||||
|
let _ = JsFuture::from(reader.cancel()).await;
|
||||||
|
return Err(RemoteFetchError::TooLarge);
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&view.to_vec());
|
||||||
|
}
|
||||||
|
let _ = window; // reader holds the stream; window kept for symmetry
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// The actual request. `GET url` with an abort signal armed at
|
/// The actual request. `GET url` with an abort signal armed at
|
||||||
/// [`TIMEOUT_MS`]; the body is refused past [`MAX_BYTES`] using the
|
/// [`TIMEOUT_MS`]; the body is refused past [`MAX_BYTES`] using the
|
||||||
/// declared `Content-Length` first and the received length second.
|
/// declared `Content-Length` first and the received length second.
|
||||||
@@ -984,8 +1189,9 @@ mod web_fetch {
|
|||||||
return Err(RemoteFetchError::Status(response.status()));
|
return Err(RemoteFetchError::Status(response.status()));
|
||||||
}
|
}
|
||||||
// Pre-check the declared size so an oversized body is refused before
|
// Pre-check the declared size so an oversized body is refused before
|
||||||
// it is buffered; the received length is checked again after the
|
// it is buffered; then STREAM the body, aborting the moment the
|
||||||
// read (chunked replies carry no Content-Length).
|
// received bytes pass MAX_BYTES — array_buffer() would materialize
|
||||||
|
// a hostile chunked reply in full before any check could fire.
|
||||||
let declared = response
|
let declared = response
|
||||||
.headers()
|
.headers()
|
||||||
.get("Content-Length")
|
.get("Content-Length")
|
||||||
@@ -995,18 +1201,7 @@ mod web_fetch {
|
|||||||
if declared.is_some_and(|declared| declared > MAX_BYTES) {
|
if declared.is_some_and(|declared| declared > MAX_BYTES) {
|
||||||
return Err(RemoteFetchError::TooLarge);
|
return Err(RemoteFetchError::TooLarge);
|
||||||
}
|
}
|
||||||
let buffer = JsFuture::from(
|
stream_body_capped(&window, &response).await
|
||||||
response
|
|
||||||
.array_buffer()
|
|
||||||
.map_err(|_| RemoteFetchError::Network)?,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|error| rejection_kind(&error))?;
|
|
||||||
let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
|
|
||||||
if bytes.len() as u64 > MAX_BYTES {
|
|
||||||
return Err(RemoteFetchError::TooLarge);
|
|
||||||
}
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the background fetch for `url` if one hasn't been attempted this
|
/// Start the background fetch for `url` if one hasn't been attempted this
|
||||||
@@ -1047,7 +1242,8 @@ fn fetch_remote(url: &str) -> Option<Vec<u8>> {
|
|||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use web_fetch::{
|
pub use web_fetch::{
|
||||||
take_outcome as take_remote_fetch_outcome, RemoteFetchError, RemoteFetchOutcome,
|
take_outcome as take_remote_fetch_outcome, take_overflowed,
|
||||||
|
RemoteFetchError, RemoteFetchOutcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
|
|||||||
Reference in new issue
Block a user