Skoroszyt Excel może nieść obrazki EMF i WMF, a konwencjonalny sposób ich narysowania to oddanie strumienia bajtów systemowemu odtwarzaczowi metafilów. To decyzja warta spojrzenia wprost: metafil to serializowany strumień poleceń dla API graficznego, a odtworzenie go znaczy pozwolenie plikowi, który przyszedł pocztą, na prowadzenie sterownika graficznego. HotXLS wybiera drugą drogę. XLSDecodeVectorScene parsuje metafil samodzielnie, waliduje nagłówek, rozmiar każdego rekordu, zadeklarowaną liczbę rekordów i dokładne umiejscowienie rekordu końca pliku, kategorycznie odrzuca rekordy escape i zwraca TXLSVectorScene prymitywnych poleceń rysowania, które backendy Canvas i SVG odtwarzają własnym kodem. Żadne odtwarzanie przez sterownik nie bierze udziału w żadnym momencie
Wymiana to pokrycie za zawieranie. Biała lista poleceń zorientowana na prostokąty nie odtworzy każdego metafila, jaki potrafi stworzyć projektant, więc scena zgłasza, ile rekordów rysowania nie potrafiła przedstawić, a wywołujący decyduje, co z tym zrobić. Dla procesu serwerowego renderującego dokumenty, których nie stworzył, ta wymiana jest właściwie ustawiona
Czemu odtwarzanie metafila słabo pasuje do niezaufanego wejścia?
Bo format nie jest obrazkiem, jest programem. Strumień rekordów EMF manipuluje stosem stanu kontekstu urządzenia, alokuje i wybiera obiekty z tablicy uchwytów i może nieść rekordy escape, których ładunek jest przekazywany do sterownika urządzenia. Odtwarzanie go ćwiczy ścieżki w platformowym stosie graficznym napisane przy założeniu, że metafil pochodzi od współpracującej aplikacji na tej samej maszynie. Gdy wejściem jest załącznik arkusza kalkulacyjnego, to założenie znika, i żadna staranność wewnątrz biblioteki arkusza nie pomoże, bo to nie biblioteka jest komponentem robiącym parsowanie
To samo rozumowanie rządzi warstwą kontenera. Skoroszyt to archiwum ZIP, a HotXLS waliduje jego katalog centralny, zamiast ufać zadeklarowanym offsetom, co opisuje artykuł o walidacji końcowego katalogu ZIP. Ładunki metafilów to następna warstwa tego samego problemu
Co dekoder sprawdza, zanim cokolwiek narysuje
Walidacja jest strukturalna i dzieje się z góry, bo parser, który zaczyna rysować i waliduje w biegu, już zadziałał na danych, których nie zweryfikował. Nagłówek musi się zgadzać ściśle, a nie wiarygodnie. Każdy rekord musi zadeklarować rozmiar mieszczący się w pozostałym buforze i wystarczająco duży na własne pola stałe. Liczba rekordów zadeklarowana w nagłówku musi się zgadzać z rekordami faktycznie obecnymi. Rekord końca pliku musi siedzieć dokładnie tam, gdzie strumień się kończy, nie jedynie gdzieś blisko, co zamyka trik śmieci końcowych ukrywających drugi ładunek za poprawnym obrazkiem
Poza strukturą dekoder jest fail-closed na semantyce. Rekordy escape są odrzucane, nie pomijane. Rekord zmieniający stan, którego dekoder nie modeluje, powoduje zawodzenie dekodowania zamiast być zignorowanym, bo ignorowanie zmiany stanu znaczy, że każde kolejne polecenie rysowania jest wykonywane w stanie, o który plik nie prosił, a wynikiem jest obrazek zły w sposób, którego nikt nie przewidzi. Rekordy rysowania poza wspieranym zestawem poleceń to inna sprawa: te są liczone i pomijane, bo brakujący kształt to widoczna, zaraportowalna luka, a nie ciche uszkodzenie
Budżety są częścią kontraktu formatu
Formaty wektorowe mają własną wersję bomby dekompresyjnej. Kilka kilobajtów rekordów może zadeklarować polilinie ze sto milionami punktów albo obrazek, którego zadeklarowane wymiary mnożą się do terabajtów. Granice muszą być więc jawnymi stałymi, a nie tym, co maszyna akurat przeżyje
// Z lxVectorScene: budżet dekodowania, wypowiedziany, a nie dorozumiany
XL_VECTOR_MAX_RECORDS = 1000000;
XL_VECTOR_MAX_HANDLES = 4096;
XL_VECTOR_MAX_DC_DEPTH = 32;
XL_VECTOR_MAX_COMMANDS = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS = 2000000;
XL_VECTOR_MAX_TEXT_CHARS = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD = 1000000000;
Dwie z nich zasługują na uwagę. Sufit głębokości kontekstu urządzenia równy 32 istnieje, bo rekordy SaveDC i RestoreDC się zagnieżdżają, a niezbalansowany strumień może pchać w nieskończoność; 32 jest hojne dla prawdziwych metafilów i tanie w egzekwowaniu. Sufit współrzędnych istnieje, bo współrzędne zasilają transformację, a wartość blisko granic zakresu liczb całkowitych daje przekształcony wynik nieskończony albo zawinięty, po czym każde obliczenie ramki ograniczającej dalej w dół to nonsens. Przycinanie współrzędnych w chwili parsowania jest znacznie łatwiejsze do przemyślenia niż bronienie każdego konsumenta geometrii
Używanie sceny
Dekoder oddaje obiekt, którego właścicielem jesteś, liczbę poleceń, rozmiar nominalny i liczbę rekordów rysowania, których nie zdecydował się przedstawić
uses
lxVectorScene;
var
Scene: TXLSVectorScene;
Error: WideString;
I: Integer;
begin
// Data trzyma surowy ładunek obrazka pobrany ze skoroszytu
if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
begin
// Odmowa: nagłówek, granice, sumy, umiejscowienie EOF albo budżet
LogReject('metafile rejected: ' + Error);
Exit;
end;
try
if Scene.SkippedDrawRecords > 0 then
LogWarning(Format('%d drawing records outside the safe subset',
[Scene.SkippedDrawRecords]));
for I := 0 to Scene.Count - 1 do
case Scene.Commands[I].Kind of
xlsvcRectangle: DrawRect(Scene.Commands[I]);
xlsvcEllipse: DrawEllipse(Scene.Commands[I]);
xlsvcPolyline,
xlsvcPolygon,
xlsvcBezier: DrawPath(Scene.Commands[I]);
xlsvcText: DrawText(Scene.Commands[I]);
xlsvcImage: DrawImage(Scene.Commands[I]);
end;
finally
Scene.Free;
end;
end;
Rekord polecenia niesie wszystko, czego backend potrzebuje, i nic, co wymaga urządzenia: obecność, kolor, szerokość i styl pióra; obecność i kolor pędzla; geometrię; a dla tekstu łańcuch, nazwę fontu, rozmiar, style i wyrównanie. To sprawia, że ta sama scena jest używalna i przez renderer canvasu na ekranie, i przez pisarza SVG, i dlatego ścieżka wektorowa nie rozjeżdża się między podglądem a eksportem. Renderowanie ekranowe zawartości arkusza ogólnie jest omówione w artykule o renderowaniu własnej siatki VCL
Odrzucenie obrazka nie uszkadza skoroszytu
Ważną własnością tego projektu jest to, że odmówione dekodowanie wpływa tylko na renderowanie. Oryginalny ładunek zostaje w modelu, więc skoroszyt otwarty i zapisany ponownie wynosi swoje obrazki metafilów bajt w bajt, niezależnie od tego, czy bezpieczny dekoder umiał je narysować. Istniejąca ograniczona ścieżka rastrowa też pozostaje dostępna jako fallback. Inaczej mówiąc, surowy parser bramkuje to, co jest wykonywane, a nie to, co jest zachowywane, i to rozróżnienie pozwala zmianie motywowanej bezpieczeństwem wyjść, nie zmieniając się w zmianę gubiącą dane
Obsługa obiektów rysunkowych ogólnie, łącznie z częściami modelu obiektów, które przechodzą podróże w obie strony nietknięte, jest omówiona w artykule o wykresach, obrazkach i rysunkach
Gdzie to zostawia wdrożenie serwerowe
Jeśli renderujesz wgrywane przez użytkowników skoroszyty w usłudze, praktyczna pozycja jest teraz obronywalna: obrazki metafilów są parsowane przez kod, który możesz audytować, ograniczone stałymi, które możesz przeczytać, i nigdy oddawane sterownikowi graficznemu. Uczciwe zastrzeżenie to pokrycie. Złożone metafile wyprodukowane przez narzędzia do rysowania trafią w licznik pominiętych rekordów, a odpowiedzią jest wyniesienie licznika na widok, a nie po cichu poszerzanie białej listy. Obrazek renderowany częściowo i mówiący o tym to rozmowa z pomocą techniczną; obrazek renderowany źle i nic nie mówiący to zgłoszenie błędu od klienta
HotXLS obsługuje XLS, XLSX, ODS i CSV natywnie w Delphi i C++Builder bez zainstalowanego Excela, a ta sama filozofia ograniczonego parsowania przechodzi przez jego warstwy kontenera, formuł i rysunków. Szczegóły formatu i bezpieczeństwa są wypisane na stronie produktu HotXLS Delphi spreadsheet component