From c49c4dffd36dad999fd19232fa96539f79109b63 Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Fri, 16 Oct 2026 02:17:40 +0200 Subject: [PATCH 1/2] 0.0.3: expand supported formats, improve viewer, harden fuzzing --- .github/workflows/ci.yml | 19 +- .github/workflows/www.yml | 12 +- .oss-scanner/Dockerfile | 2 +- CHANGELOG.md | 478 + Cargo.lock | 852 +- Cargo.toml | 13 +- Dockerfile | 4 +- Makefile | 55 +- NOTICE | 242 +- README.md | 5 +- crates/exav-core/Cargo.toml | 48 +- crates/exav-core/src/engine/mod.rs | 47 + crates/exav-core/src/jsnorm.rs | 9 +- crates/exav-core/src/lib.rs | 504 +- crates/exav-core/src/pe.rs | 281 +- crates/exav-core/src/phishing.rs | 24 + crates/exav-core/src/yara/compiler.rs | 127 + .../fixtures/fuzz/yara_nested_parens.yar | Bin 0 -> 3521 bytes .../exav-core/tests/fixtures/volumes/set4.r00 | Bin 0 -> 8192 bytes .../exav-core/tests/fixtures/volumes/set4.r01 | Bin 0 -> 6508 bytes .../exav-core/tests/fixtures/volumes/set4.rar | Bin 0 -> 8192 bytes .../tests/fixtures/volumes/set5.part1.rar | Bin 0 -> 8192 bytes .../tests/fixtures/volumes/set5.part2.rar | Bin 0 -> 8192 bytes .../tests/fixtures/volumes/set5.part3.rar | Bin 0 -> 6702 bytes .../tests/fixtures/volumes/split.z01 | Bin 0 -> 65536 bytes .../tests/fixtures/volumes/split.zip | Bin 0 -> 34640 bytes .../exav-core/tests/suites/embedded_carve.rs | 74 + crates/exav-core/tests/suites/exec_offsets.rs | 370 + .../exav-core/tests/suites/format_registry.rs | 82 + crates/exav-core/tests/suites/mod.rs | 9 + crates/exav-core/tests/suites/split_sets.rs | 305 + crates/exav-core/tests/suites/target_text.rs | 24 +- crates/exav-core/tests/suites/visible_text.rs | 345 + .../tests/suites/visible_text_office.rs | 138 + crates/exav-grep/Cargo.toml | 15 +- crates/exav-grep/src/lib.rs | 91 +- crates/exav-grep/src/main.rs | 13 + crates/exav-grep/tests/text.rs | 178 + crates/exav-grep/tests/text_office.rs | 129 + crates/exav-pe-emu/NOTICE | 86 +- crates/exav-pe-emu/scripts/gen-exports.py | 152 + crates/exav-pe-emu/src/cpu.rs | 38 +- crates/exav-pe-emu/src/exports/advapi32.txt | 541 + crates/exav-pe-emu/src/exports/kernel32.txt | 1267 + crates/exav-pe-emu/src/exports/ntdll.txt | 133 + crates/exav-pe-emu/src/exports/user32.txt | 755 + crates/exav-pe-emu/src/image.rs | 29 +- crates/exav-pe-emu/src/run.rs | 234 +- crates/exav-pe-emu/src/win.rs | 392 +- crates/exav-pe-emu/tests/end_to_end.rs | 249 + crates/exav-render/Cargo.toml | 191 +- crates/exav-render/NOTICE | 231 + crates/exav-render/README.md | 86 +- .../examples/office_docs_convert.rs | 108 + .../examples/office_slides_convert.rs | 106 + crates/exav-render/examples/office_text.rs | 87 + crates/exav-render/examples/raw_preview.rs | 73 + crates/exav-render/examples/scene_stats.rs | 4 +- crates/exav-render/scripts/font-tables.py | 8 +- .../scripts/office/common/codepages.py | 60 + .../scripts/office/sheets/ftab.json | 375 + .../exav-render/scripts/office/sheets/ftab.py | 22 + .../scripts/office/sheets/numbers_fonts.py | 43 + .../office/sheets/numbers_functions.py | 23 + .../exav-render/src/formats/cad/dwg/blocks.rs | 84 +- .../src/formats/cad/dwg/entities.rs | 65 +- .../exav-render/src/formats/cad/dwg/header.rs | 7 +- .../src/formats/cad/dwg/objects.rs | 61 + .../exav-render/src/formats/cad/dwg/tables.rs | 54 +- .../exav-render/src/formats/cad/dxf/build.rs | 21 + .../src/formats/cad/dxf/entities.rs | 98 +- .../exav-render/src/formats/cad/dxf/header.rs | 1 + crates/exav-render/src/formats/cad/dxf/mod.rs | 7 + .../src/formats/cad/dxf/objects.rs | 36 + .../exav-render/src/formats/cad/dxf/tables.rs | 6 + crates/exav-render/src/formats/cad/json.rs | 34 + crates/exav-render/src/formats/cad/model.rs | 3 + .../src/formats/cad/model/entity.rs | 95 + .../src/formats/cad/model/tables.rs | 17 + crates/exav-render/src/formats/dwg/DESIGN.md | 40 +- crates/exav-render/src/formats/dwg/clip.rs | 11 + crates/exav-render/src/formats/dwg/curves.rs | 76 +- .../exav-render/src/formats/dwg/dimension.rs | 471 + crates/exav-render/src/formats/dwg/font.rs | 89 + .../exav-render/src/formats/dwg/linetype.rs | 90 +- crates/exav-render/src/formats/dwg/metrics.rs | 1592 +- crates/exav-render/src/formats/dwg/mod.rs | 105 +- crates/exav-render/src/formats/dwg/scene.rs | 882 +- .../exav-render/src/formats/dwg/tessellate.rs | 2000 +- crates/exav-render/src/formats/dwg/text.rs | 369 +- crates/exav-render/src/formats/dwg/tiles.rs | 8 +- crates/exav-render/src/formats/dwg/wide.rs | 277 + crates/exav-render/src/formats/gltf.rs | 916 + crates/exav-render/src/formats/gltf/json.rs | 288 + .../exav-render/src/formats/ifc/alignment.rs | 464 + crates/exav-render/src/formats/ifc/curves.rs | 62 +- .../exav-render/src/formats/ifc/directrix.rs | 235 + crates/exav-render/src/formats/ifc/faces.rs | 1002 + crates/exav-render/src/formats/ifc/grid.rs | 254 + crates/exav-render/src/formats/ifc/mod.rs | 89 +- crates/exav-render/src/formats/ifc/solids.rs | 243 +- crates/exav-render/src/formats/ifc/spline.rs | 325 + crates/exav-render/src/formats/image.rs | 38 +- crates/exav-render/src/formats/image/jxl.rs | 46 + crates/exav-render/src/formats/image/raw.rs | 545 + .../exav-render/src/formats/image/raw/bmff.rs | 92 + .../exav-render/src/formats/image/raw/jpeg.rs | 151 + .../src/formats/image/raw/tests.rs | 777 + .../exav-render/src/formats/image/raw/tiff.rs | 337 + crates/exav-render/src/formats/markdown.rs | 877 + crates/exav-render/src/formats/mesh.rs | 36 +- crates/exav-render/src/formats/mod.rs | 26 +- crates/exav-render/src/formats/obj.rs | 197 + .../src/formats/office/common/base64.rs | 49 + .../src/formats/office/common/bytes.rs | 119 + .../src/formats/office/common/cfb.rs | 510 + .../src/formats/office/common/codepage.rs | 93 + .../formats/office/common/codepage_tables.rs | 386 + .../src/formats/office/common/escher.rs | 633 + .../src/formats/office/common/image.rs | 288 + .../src/formats/office/common/iwa/mod.rs | 103 + .../src/formats/office/common/iwa/proto.rs | 252 + .../src/formats/office/common/iwa/snappy.rs | 149 + .../src/formats/office/common/mod.rs | 22 + .../src/formats/office/common/odf.rs | 415 + .../src/formats/office/common/package.rs | 187 + .../src/formats/office/common/xml.rs | 416 + .../src/formats/office/common/xmlout.rs | 43 + .../src/formats/office/docs/doc/draw.rs | 365 + .../src/formats/office/docs/doc/fib.rs | 292 + .../src/formats/office/docs/doc/lists.rs | 370 + .../src/formats/office/docs/doc/mod.rs | 572 + .../src/formats/office/docs/doc/props.rs | 870 + .../src/formats/office/docs/doc/sprm.rs | 140 + .../src/formats/office/docs/doc/story.rs | 1236 + .../src/formats/office/docs/doc/styles.rs | 184 + .../src/formats/office/docs/doc/tables.rs | 520 + .../src/formats/office/docs/doc/word6.rs | 1311 + .../src/formats/office/docs/docx/body.rs | 578 + .../src/formats/office/docs/docx/mod.rs | 195 + .../src/formats/office/docs/docx/parts.rs | 318 + .../src/formats/office/docs/docx/props.rs | 649 + .../src/formats/office/docs/error.rs | 70 + .../src/formats/office/docs/fields.rs | 77 + .../src/formats/office/docs/grid.rs | 169 + .../src/formats/office/docs/limits.rs | 126 + .../src/formats/office/docs/mod.rs | 132 + .../src/formats/office/docs/model.rs | 871 + .../src/formats/office/docs/odt/frame.rs | 294 + .../src/formats/office/docs/odt/list.rs | 233 + .../src/formats/office/docs/odt/mod.rs | 675 + .../src/formats/office/docs/odt/props.rs | 418 + .../src/formats/office/docs/odt/table.rs | 345 + .../src/formats/office/docs/odt/text.rs | 651 + .../src/formats/office/docs/pages/mod.rs | 542 + .../src/formats/office/docs/rtf/lex.rs | 181 + .../src/formats/office/docs/rtf/mod.rs | 1124 + .../src/formats/office/docs/rtf/pict.rs | 185 + .../src/formats/office/docs/rtf/words.rs | 1287 + .../src/formats/office/docs/text.rs | 1423 + .../src/formats/office/docs/wml/mod.rs | 702 + .../src/formats/office/docs/wml/props.rs | 424 + crates/exav-render/src/formats/office/mod.rs | 42 + .../src/formats/office/sheets/charts.rs | 153 + .../src/formats/office/sheets/css.rs | 439 + .../src/formats/office/sheets/error.rs | 48 + .../src/formats/office/sheets/fonts.rs | 27 + .../src/formats/office/sheets/geom.rs | 122 + .../src/formats/office/sheets/html.rs | 2016 ++ .../src/formats/office/sheets/html/tests.rs | 377 + .../src/formats/office/sheets/limits.rs | 105 + .../src/formats/office/sheets/mod.rs | 161 + .../src/formats/office/sheets/model.rs | 830 + .../formats/office/sheets/numbers/cells.rs | 294 + .../formats/office/sheets/numbers/fonts.rs | 155 + .../formats/office/sheets/numbers/format.rs | 585 + .../formats/office/sheets/numbers/formula.rs | 621 + .../office/sheets/numbers/functions.rs | 369 + .../src/formats/office/sheets/numbers/mod.rs | 469 + .../formats/office/sheets/numbers/style.rs | 305 + .../formats/office/sheets/numbers/table.rs | 954 + .../formats/office/sheets/numbers/tests.rs | 319 + .../src/formats/office/sheets/numbers/v09.rs | 1163 + .../office/sheets/numbers/v09/tests.rs | 413 + .../src/formats/office/sheets/numtext.rs | 76 + .../src/formats/office/sheets/ods/cf.rs | 684 + .../src/formats/office/sheets/ods/chart.rs | 331 + .../src/formats/office/sheets/ods/dom.rs | 284 + .../src/formats/office/sheets/ods/draw.rs | 488 + .../src/formats/office/sheets/ods/formula.rs | 606 + .../src/formats/office/sheets/ods/mod.rs | 713 + .../src/formats/office/sheets/ods/numfmt.rs | 409 + .../src/formats/office/sheets/ods/styles.rs | 615 + .../src/formats/office/sheets/ods/table.rs | 852 + .../src/formats/office/sheets/r1c1.rs | 274 + .../src/formats/office/sheets/sylk.rs | 791 + .../src/formats/office/sheets/sylk/tests.rs | 173 + .../src/formats/office/sheets/text.rs | 490 + .../src/formats/office/sheets/textenc.rs | 155 + .../src/formats/office/sheets/xls/cf.rs | 722 + .../src/formats/office/sheets/xls/chart.rs | 454 + .../src/formats/office/sheets/xls/crypto.rs | 520 + .../src/formats/office/sheets/xls/escher.rs | 557 + .../src/formats/office/sheets/xls/formula.rs | 800 + .../src/formats/office/sheets/xls/ftab.rs | 379 + .../src/formats/office/sheets/xls/mod.rs | 1326 + .../src/formats/office/sheets/xls/sheet.rs | 964 + .../src/formats/office/sheets/xls/stream.rs | 119 + .../src/formats/office/sheets/xls/tests.rs | 251 + .../src/formats/office/sheets/xlsx/chart.rs | 324 + .../src/formats/office/sheets/xlsx/drawing.rs | 306 + .../src/formats/office/sheets/xlsx/mod.rs | 1062 + .../src/formats/office/sheets/xlsx/styles.rs | 351 + .../src/formats/office/sheets/xlsx/xml.rs | 72 + .../src/formats/office/sheets/xmlss.rs | 862 + .../src/formats/office/sheets/xmlss/tests.rs | 281 + .../src/formats/office/slides/error.rs | 58 + .../src/formats/office/slides/keynote/apxl.rs | 1305 + .../formats/office/slides/keynote/apxl1.rs | 914 + .../formats/office/slides/keynote/chart.rs | 249 + .../src/formats/office/slides/keynote/mod.rs | 612 + .../formats/office/slides/keynote/shapes.rs | 600 + .../formats/office/slides/keynote/table.rs | 814 + .../src/formats/office/slides/keynote/text.rs | 437 + .../src/formats/office/slides/limits.rs | 126 + .../src/formats/office/slides/mod.rs | 205 + .../src/formats/office/slides/model.rs | 594 + .../src/formats/office/slides/odp/geom.rs | 1035 + .../src/formats/office/slides/odp/mod.rs | 1869 ++ .../src/formats/office/slides/odp/styles.rs | 378 + .../src/formats/office/slides/odp/text.rs | 565 + .../src/formats/office/slides/package.rs | 101 + .../src/formats/office/slides/ppt/escher.rs | 514 + .../src/formats/office/slides/ppt/mod.rs | 1380 + .../src/formats/office/slides/ppt/old.rs | 163 + .../src/formats/office/slides/ppt/ole.rs | 136 + .../src/formats/office/slides/ppt/pp9.rs | 220 + .../src/formats/office/slides/ppt/ppt95.rs | 768 + .../src/formats/office/slides/ppt/presets.rs | 309 + .../src/formats/office/slides/ppt/records.rs | 125 + .../src/formats/office/slides/ppt/shapes.rs | 785 + .../src/formats/office/slides/ppt/text.rs | 1023 + .../src/formats/office/slides/pptx/draw.rs | 701 + .../src/formats/office/slides/pptx/mod.rs | 532 + .../src/formats/office/slides/pptx/text.rs | 315 + .../src/formats/office/slides/pptx/xml.rs | 51 + .../src/formats/office/slides/text.rs | 178 + .../src/formats/office/slides/vsdx/convert.rs | 517 + .../src/formats/office/slides/vsdx/format.rs | 419 + .../src/formats/office/slides/vsdx/formula.rs | 645 + .../src/formats/office/slides/vsdx/geom.rs | 1146 + .../src/formats/office/slides/vsdx/mod.rs | 614 + .../src/formats/office/slides/vsdx/resolve.rs | 984 + .../src/formats/office/slides/vsdx/sheet.rs | 339 + .../src/formats/office/slides/vsdx/text.rs | 362 + crates/exav-render/src/formats/office/text.rs | 230 + .../exav-render/src/formats/office/visible.rs | 241 + .../src/formats/pdf_image/jbig2.rs | 6 +- crates/exav-render/src/formats/ply.rs | 469 + crates/exav-render/src/formats/soup.rs | 283 + crates/exav-render/src/formats/text.rs | 206 + crates/exav-render/src/image_codecs/README.md | 5 +- .../src/image_codecs/tiff/bytecast.rs | 11 +- .../src/image_codecs/tiff/decoder/mod.rs | 6 +- crates/exav-render/src/lib.rs | 42 +- crates/exav-render/tests/cad_dwg_oracle.rs | 9 + crates/exav-render/tests/cad_oracle.rs | 35 + .../tests/cad_oracle/ezdxf_dump.py | 54 +- crates/exav-render/tests/cad_proxy.rs | 51 +- crates/exav-render/tests/dwg.rs | 9 +- crates/exav-render/tests/dwg_fidelity.rs | 675 + crates/exav-render/tests/dwg_memory.rs | 63 + crates/exav-render/tests/dwg_text.rs | 293 + .../tests/fixtures/dwg/circle.dxf.gz | Bin 0 -> 3805 bytes .../tests/fixtures/dwg/colours.dxf.gz | Bin 0 -> 3902 bytes .../fixtures/dwg/complex_linetypes.dwg.gz | Bin 0 -> 9561 bytes .../fixtures/dwg/complex_linetypes.dxf.gz | Bin 0 -> 4179 bytes .../tests/fixtures/dwg/dense_hatch.dxf.gz | Bin 0 -> 3937 bytes .../tests/fixtures/dwg/dimensions.dxf.gz | Bin 0 -> 4235 bytes .../tests/fixtures/dwg/gaps.dwg.gz | Bin 0 -> 10714 bytes .../tests/fixtures/dwg/gaps.dxf.gz | Bin 0 -> 4931 bytes .../tests/fixtures/dwg/halo.dxf.gz | Bin 0 -> 3844 bytes crates/exav-render/tests/fixtures/dwg/make.py | 486 + .../tests/fixtures/dwg/not_drawn.dwg.gz | Bin 0 -> 11785 bytes .../tests/fixtures/dwg/not_drawn.dxf.gz | Bin 0 -> 6165 bytes .../tests/fixtures/dwg/plinegen.dwg.gz | Bin 0 -> 8982 bytes .../tests/fixtures/dwg/plinegen.dxf.gz | Bin 0 -> 3872 bytes .../tests/fixtures/dwg/sheets.dxf.gz | Bin 0 -> 4355 bytes .../tests/fixtures/dwg/solid_only.dxf.gz | Bin 0 -> 5481 bytes .../tests/fixtures/dwg/stray.dxf.gz | Bin 0 -> 5863 bytes .../tests/fixtures/dwg/text_frames.dxf.gz | Bin 0 -> 4092 bytes .../tests/fixtures/dwg/text_many.dxf.gz | Bin 0 -> 6138 bytes .../tests/fixtures/dwg/text_mtext.dxf.gz | Bin 0 -> 4597 bytes .../tests/fixtures/dwg/text_symbols.dxf.gz | Bin 0 -> 4131 bytes .../tests/fixtures/dwg/transparency.dwg.gz | Bin 0 -> 9206 bytes .../tests/fixtures/dwg/transparency.dxf.gz | Bin 0 -> 4056 bytes .../tests/fixtures/dwg/viewport_text.dxf.gz | Bin 0 -> 4196 bytes .../tests/fixtures/dwg/wide.dxf.gz | Bin 0 -> 3864 bytes .../tests/fixtures/dwg/xclip-R2000.dwg.gz | Bin 0 -> 10254 bytes .../tests/fixtures/dwg/xclip.dwg.gz | Bin 0 -> 9564 bytes .../tests/fixtures/dwg/xclip.dxf.gz | Bin 0 -> 4262 bytes .../tests/fixtures/ifc/alignment.ifc | 117 + .../tests/fixtures/ifc/curved_faces.ifc | 384 + .../tests/fixtures/ifc/expected.json | 398 + .../exav-render/tests/fixtures/ifc/grid.ifc | 112 + .../tests/fixtures/ifc/grid_ifc4x3.ifc | 58 + crates/exav-render/tests/fixtures/ifc/make.py | 467 +- .../tests/fixtures/ifc/storeys.ifc | 96 + .../tests/fixtures/ifc/sweep_params.ifc | 149 + .../tests/fixtures/images/jxl/big-flat.jxl | Bin 0 -> 12247 bytes .../fixtures/images/jxl/grey-lossless.jxl | Bin 0 -> 910 bytes .../tests/fixtures/images/jxl/mid-grey.jxl | Bin 0 -> 139304 bytes .../fixtures/images/jxl/rgb-container.jxl | Bin 0 -> 1936 bytes .../fixtures/images/jxl/rgb-lossless.jxl | Bin 0 -> 1896 bytes .../fixtures/images/jxl/rgb-lossy-e3.jxl | Bin 0 -> 704 bytes .../fixtures/images/jxl/rgb-lossy-e3.ref.ppm | Bin 0 -> 9229 bytes .../tests/fixtures/images/jxl/rgb-lossy.jxl | Bin 0 -> 1555 bytes .../fixtures/images/jxl/rgb-lossy.ref.ppm | Bin 0 -> 9229 bytes .../fixtures/images/jxl/rgba-lossless.jxl | Bin 0 -> 1955 bytes .../exav-render/tests/fixtures/images/rgb.jxl | Bin 0 -> 620 bytes .../tests/fixtures/images/rgba.jxl | Bin 0 -> 424 bytes .../tests/fixtures/office/docs/make.py | 97 + .../fixtures/office/docs/sample-2003.xml | 9 + .../tests/fixtures/office/docs/sample.doc | Bin 0 -> 11264 bytes .../tests/fixtures/office/docs/sample.fodt | 26 + .../tests/fixtures/office/docs/sample.odt | Bin 0 -> 1925 bytes .../tests/fixtures/office/docs/sample.rtf | 13 + .../tests/fixtures/office/draw/README.md | 22 + .../tests/fixtures/office/draw/make_vsdx.py | 450 + .../tests/fixtures/office/draw/sample.fodg | 56 + .../tests/fixtures/office/draw/sample.odg | Bin 0 -> 17515 bytes .../tests/fixtures/office/draw/sample.vsdx | Bin 0 -> 5739 bytes .../tests/fixtures/office/draw/themed.vsdx | Bin 0 -> 3227 bytes .../tests/fixtures/office/sheets/README.md | 23 + .../fixtures/office/sheets/numbers09.numbers | Bin 0 -> 134571 bytes .../fixtures/office/sheets/sample-2003.xml | 62 + .../tests/fixtures/office/sheets/sample.fods | 56 + .../tests/fixtures/office/sheets/sample.html | 93 + .../fixtures/office/sheets/sample.numbers | Bin 0 -> 25371 bytes .../tests/fixtures/office/sheets/sample.ods | Bin 0 -> 14404 bytes .../tests/fixtures/office/sheets/sample.slk | 33 + .../tests/fixtures/office/sheets/sample.xls | Bin 0 -> 7168 bytes .../tests/fixtures/office/slides/README.md | 30 + .../tests/fixtures/office/slides/chart.fodp | 53 + .../tests/fixtures/office/slides/chart.odp | Bin 0 -> 1685 bytes .../tests/fixtures/office/slides/chart.ppt | Bin 0 -> 16896 bytes .../tests/fixtures/office/slides/pictures.odp | Bin 0 -> 189170 bytes .../tests/fixtures/office/slides/pictures.ppt | Bin 0 -> 203776 bytes .../tests/fixtures/office/slides/sample.fodp | 31 + .../tests/fixtures/office/slides/sample.key | Bin 0 -> 329209 bytes .../tests/fixtures/office/slides/sample.odp | Bin 0 -> 16195 bytes .../tests/fixtures/office/slides/sample.ppt | Bin 0 -> 461312 bytes .../tests/fixtures/office/slides/table.key | Bin 0 -> 90813 bytes .../raw/camera-structure-no-preview.tif | Bin 0 -> 638 bytes .../tests/fixtures/raw/canon-heif.cr3 | Bin 0 -> 2838 bytes .../tests/fixtures/raw/canon-o1.cr3 | Bin 0 -> 4677 bytes .../tests/fixtures/raw/canon-o8.cr3 | Bin 0 -> 4677 bytes .../exav-render/tests/fixtures/raw/canon.cr2 | Bin 0 -> 4616 bytes .../exav-render/tests/fixtures/raw/canon.crw | Bin 0 -> 1696 bytes .../tests/fixtures/raw/cfa-tags-only.tif | Bin 0 -> 2866 bytes .../tests/fixtures/raw/compression-32767.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-32769.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-32772.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-34713.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-65000.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-65535.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/compression-99.tif | Bin 0 -> 2842 bytes .../tests/fixtures/raw/fujifilm-o1.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o2.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o3.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o4.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o5.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o6.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o7.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/fujifilm-o8.raf | Bin 0 -> 3043 bytes .../tests/fixtures/raw/hasselblad.3fr | Bin 0 -> 3212 bytes .../tests/fixtures/raw/kodak16-be.dcr | Bin 0 -> 3488 bytes .../tests/fixtures/raw/kodak16-le.dcr | Bin 0 -> 3488 bytes .../tests/fixtures/raw/lossy-linear.dng | Bin 0 -> 2108 bytes crates/exav-render/tests/fixtures/raw/make.py | 465 + .../tests/fixtures/raw/many-markers.dng | Bin 0 -> 4518 bytes .../tests/fixtures/raw/minolta.mrw | Bin 0 -> 556 bytes .../exav-render/tests/fixtures/raw/nikon.nef | Bin 0 -> 3798 bytes .../tests/fixtures/raw/no-preview.dng | Bin 0 -> 2158 bytes .../tests/fixtures/raw/olympus-be.orf | Bin 0 -> 2708 bytes .../tests/fixtures/raw/olympus-rs.orf | Bin 0 -> 2708 bytes .../tests/fixtures/raw/olympus.orf | Bin 0 -> 2708 bytes .../tests/fixtures/raw/orientation-own.dng | Bin 0 -> 3050 bytes .../fixtures/raw/panasonic-no-preview.rw2 | Bin 0 -> 26 bytes .../tests/fixtures/raw/panasonic.rw2 | Bin 0 -> 980 bytes .../exav-render/tests/fixtures/raw/pentax.pef | Bin 0 -> 3844 bytes .../tests/fixtures/raw/phaseone.iiq | Bin 0 -> 1808 bytes .../fixtures/raw/photometric-cfa-only.tif | Bin 0 -> 2854 bytes .../tests/fixtures/raw/plain-with-make.tif | Bin 0 -> 758 bytes .../exav-render/tests/fixtures/raw/plain.tif | Bin 0 -> 728 bytes .../tests/fixtures/raw/preview-grey.dng | Bin 0 -> 326 bytes .../tests/fixtures/raw/preview-jpeg.dng | Bin 0 -> 2164 bytes .../tests/fixtures/raw/preview-rgb.dng | Bin 0 -> 5370 bytes .../tests/fixtures/raw/private-data-only.tif | Bin 0 -> 740 bytes .../tests/fixtures/raw/progressive.dng | Bin 0 -> 2854 bytes .../tests/fixtures/raw/samsung.srw | Bin 0 -> 3562 bytes .../exav-render/tests/fixtures/raw/sigma.x3f | Bin 0 -> 3134 bytes .../tests/fixtures/raw/sony-thumbnail.srf | Bin 0 -> 832 bytes .../exav-render/tests/fixtures/raw/sony.arw | Bin 0 -> 3266 bytes crates/exav-render/tests/gltf.rs | 927 + crates/exav-render/tests/ifc.rs | 8 +- crates/exav-render/tests/ifc_geometry.rs | 414 + crates/exav-render/tests/ifc_hostile.rs | 500 + crates/exav-render/tests/jpeg2000_jbig2.rs | 25 + crates/exav-render/tests/jxl.rs | 300 + crates/exav-render/tests/markdown.rs | 938 + crates/exav-render/tests/markdown_text.rs | 146 + crates/exav-render/tests/mesh_common/mod.rs | 190 + crates/exav-render/tests/obj.rs | 477 + crates/exav-render/tests/office_common/mod.rs | 487 + crates/exav-render/tests/office_docs_doc.rs | 467 + .../exav-render/tests/office_docs_formats.rs | 557 + crates/exav-render/tests/office_sheets.rs | 273 + crates/exav-render/tests/office_slides.rs | 503 + .../exav-render/tests/office_slides_draw.rs | 143 + .../tests/office_slides_keynote_xml.rs | 436 + .../tests/office_slides_odp_features.rs | 267 + .../tests/office_slides_old_ppt.rs | 289 + .../exav-render/tests/office_slides_vsdx.rs | 832 + crates/exav-render/tests/office_text_docs.rs | 1029 + .../exav-render/tests/office_text_sheets.rs | 819 + .../exav-render/tests/office_text_slides.rs | 254 + crates/exav-render/tests/ply.rs | 633 + crates/exav-render/tests/raw.rs | 705 + crates/exav-render/tests/text.rs | 100 + crates/exav-unpack-wasm/Cargo.lock | 181 +- crates/exav-unpack-wasm/Cargo.toml | 2 +- crates/exav-unpack-wasm/NOTICE | 56 +- crates/exav-unpack-wasm/package-lock.json | 746 +- crates/exav-unpack-wasm/package.json | 4 +- crates/exav-unpack/Cargo.toml | 72 +- crates/exav-unpack/MEMORY.md | 17 +- crates/exav-unpack/NOTICE | 60 +- crates/exav-unpack/README.md | 5 + crates/exav-unpack/src/formats/cab.rs | 23 + crates/exav-unpack/src/formats/cab_join.rs | 417 + .../src/formats/cab_parse/ctype.rs | 63 +- .../src/formats/cab_parse/folder.rs | 28 +- .../src/formats/cab_parse/mszip.rs | 70 +- crates/exav-unpack/src/formats/dcl_explode.rs | 334 + crates/exav-unpack/src/formats/ext.rs | 74 +- crates/exav-unpack/src/formats/fat.rs | 87 +- crates/exav-unpack/src/formats/ishield_z.rs | 242 +- crates/exav-unpack/src/formats/lzfse.rs | 1075 + crates/exav-unpack/src/formats/mod.rs | 23 +- crates/exav-unpack/src/formats/ntfs.rs | 423 +- crates/exav-unpack/src/formats/partition.rs | 21 +- crates/exav-unpack/src/formats/pdf.rs | 10 +- .../src/formats/pdf_parse/crypt.rs | 27 + .../exav-unpack/src/formats/pdf_parse/lex.rs | 47 +- crates/exav-unpack/src/formats/ppmd8/mod.rs | 8 +- crates/exav-unpack/src/formats/qcow2.rs | 665 +- crates/exav-unpack/src/formats/rar.rs | 8 +- crates/exav-unpack/src/formats/rar3_crypt.rs | 42 +- crates/exav-unpack/src/formats/rar5_crypt.rs | 88 +- crates/exav-unpack/src/formats/reported.rs | 3 +- crates/exav-unpack/src/formats/udif.rs | 44 +- crates/exav-unpack/src/formats/vdisk.rs | 667 + crates/exav-unpack/src/formats/vhd.rs | 428 +- crates/exav-unpack/src/formats/vhdx.rs | 351 +- crates/exav-unpack/src/formats/vmdk.rs | 737 +- crates/exav-unpack/src/formats/volumes.rs | 299 + crates/exav-unpack/src/formats/zip.rs | 186 +- crates/exav-unpack/src/formats/zstd.rs | 27 + crates/exav-unpack/src/lib.rs | 49 +- crates/exav-unpack/src/oracle.rs | 151 + crates/exav-unpack/src/stream.rs | 14 +- crates/exav-unpack/src/text/html.rs | 178 + crates/exav-unpack/src/text/mod.rs | 352 + crates/exav-unpack/src/text/pdf.rs | 156 + crates/exav-unpack/src/text/pdf_screen.rs | 181 + crates/exav-unpack/src/text/xml.rs | 109 + crates/exav-unpack/tests/fixtures/README.md | 12 + .../diskimage/fat4g.deflate.qcow2.gz.xor | Bin 0 -> 13218 bytes .../diskimage/fat4g.dynamic.vhd.gz.xor | Bin 0 -> 18473 bytes .../diskimage/fat4g.dynamic.vhdx.gz.xor | Bin 0 -> 24772 bytes .../diskimage/fat4g.plain.qcow2.gz.xor | Bin 0 -> 12904 bytes .../diskimage/fat4g.sparse.vmdk.gz.xor | Bin 0 -> 15206 bytes .../diskimage/fat4g.stream.vmdk.gz.xor | Bin 0 -> 15288 bytes .../diskimage/fat4g.zstd.qcow2.gz.xor | Bin 0 -> 12998 bytes .../tests/fixtures/dmg/udif_lzfse.dmg | Bin 0 -> 2727 bytes crates/exav-unpack/tests/oracle/build.sh | 52 + crates/exav-unpack/tests/oracle/dcl_oracle.c | 69 + .../exav-unpack/tests/oracle/lzfse_oracle.c | 55 + .../exav-unpack/tests/suites/cab_salvage.rs | 127 + .../exav-unpack/tests/suites/cab_volumes.rs | 549 + crates/exav-unpack/tests/suites/ext.rs | 2 +- crates/exav-unpack/tests/suites/extreme.rs | 111 +- crates/exav-unpack/tests/suites/fat.rs | 61 + crates/exav-unpack/tests/suites/ishield_z.rs | 144 + crates/exav-unpack/tests/suites/lzfse.rs | 65 + crates/exav-unpack/tests/suites/mod.rs | 17 + crates/exav-unpack/tests/suites/text.rs | 513 + .../exav-unpack/tests/suites/virtual_disks.rs | 1157 + crates/exav-viewer/.gitignore | 2 + crates/exav-viewer/Cargo.lock | 382 +- crates/exav-viewer/Cargo.toml | 23 +- crates/exav-viewer/LICENSES/BSD-3-Clause.txt | 27 + crates/exav-viewer/NOTICE | 85 +- crates/exav-viewer/README.md | 133 +- crates/exav-viewer/demo/examples/NOTICE.md | 2912 ++ crates/exav-viewer/demo/examples/check.mjs | 114 + .../exav-viewer/demo/examples/examples.json | 4703 +++ .../examples/files/archives/encrypted-aes.zip | Bin 0 -> 16604 bytes .../files/archives/encrypted-zipcrypto.zip | Bin 0 -> 16262 bytes .../demo/examples/files/archives/encrypted.7z | Bin 0 -> 13726 bytes .../demo/examples/files/archives/example.jar | Bin 0 -> 793 bytes .../examples/files/archives/exav-example.deb | Bin 0 -> 2256 bytes .../demo/examples/files/archives/libexample.a | 40 + .../examples/files/archives/matryoshka.zip | Bin 0 -> 7232 bytes .../demo/examples/files/archives/pages.cbz | Bin 0 -> 10720 bytes .../demo/examples/files/archives/sample.7z | Bin 0 -> 13642 bytes .../demo/examples/files/archives/sample.cab | Bin 0 -> 6284 bytes .../demo/examples/files/archives/sample.cpio | Bin 0 -> 7168 bytes .../demo/examples/files/archives/sample.iso | Bin 0 -> 133120 bytes .../demo/examples/files/archives/sample.rar | Bin 0 -> 35056 bytes .../demo/examples/files/archives/sample.tar | Bin 0 -> 51200 bytes .../examples/files/archives/sample.tar.bz2 | Bin 0 -> 14648 bytes .../examples/files/archives/sample.tar.gz | Bin 0 -> 15648 bytes .../examples/files/archives/sample.tar.xz | Bin 0 -> 13772 bytes .../examples/files/archives/sample.tar.zst | Bin 0 -> 14331 bytes .../demo/examples/files/archives/sample.zip | Bin 0 -> 15824 bytes .../examples/files/archives/server.log.bz2 | Bin 0 -> 1155 bytes .../examples/files/archives/server.log.gz | Bin 0 -> 1331 bytes .../examples/files/archives/server.log.xz | Bin 0 -> 820 bytes .../demo/examples/files/cad/blocks.dxf | 6052 ++++ .../demo/examples/files/cad/bracket.dxf | 8288 +++++ .../demo/examples/files/cad/dimensions.dxf | 10562 ++++++ .../demo/examples/files/cad/hatch-gallery.dxf | 26910 ++++++++++++++++ .../examples/files/cad/house-elevations.dxf | 18904 +++++++++++ .../examples/files/cad/house-plan-r2000.dxf | 16378 ++++++++++ .../demo/examples/files/cad/house-plan.dxf | 17000 ++++++++++ .../demo/examples/files/cad/legacy-r12.dxf | 2602 ++ .../demo/examples/files/cad/linetypes.dxf | 7112 ++++ .../demo/examples/files/cad/site-plan.dxf | 14098 ++++++++ .../demo/examples/files/cad/text-styles.dxf | 5696 ++++ .../demo/examples/files/converted/deck.fodp | 54 + .../demo/examples/files/converted/deck.odp | Bin 0 -> 72325 bytes .../demo/examples/files/converted/deck.ppt | Bin 0 -> 491520 bytes .../examples/files/converted/diagram.fodg | 28 + .../demo/examples/files/converted/diagram.odg | Bin 0 -> 26758 bytes .../examples/files/converted/handbook.doc | Bin 0 -> 67584 bytes .../examples/files/converted/handbook.fodt | 99 + .../examples/files/converted/handbook.odt | Bin 0 -> 42008 bytes .../examples/files/converted/handbook.rtf | 620 + .../examples/files/converted/workbook.fods | 62 + .../examples/files/converted/workbook.ods | Bin 0 -> 45583 bytes .../examples/files/converted/workbook.xls | Bin 0 -> 29696 bytes .../demo/examples/files/ifc/house.ifc | 1438 + .../demo/examples/files/images/bounce.gif | Bin 0 -> 102524 bytes .../demo/examples/files/images/bounce.jxl | Bin 0 -> 61211 bytes .../demo/examples/files/images/bounce.png | Bin 0 -> 41428 bytes .../demo/examples/files/images/bounce.webp | Bin 0 -> 20748 bytes .../demo/examples/files/images/fax-g3-1d.tif | Bin 0 -> 61464 bytes .../examples/files/images/fax-g4-3pages.tif | Bin 0 -> 68092 bytes .../demo/examples/files/images/fax-g4.tif | Bin 0 -> 22702 bytes .../examples/files/images/gradient-grey16.png | Bin 0 -> 48383 bytes .../examples/files/images/gradient-grey16.tif | Bin 0 -> 93424 bytes .../examples/files/images/icon-legacy.ico | Bin 0 -> 14193 bytes .../demo/examples/files/images/icon.ico | Bin 0 -> 25530 bytes .../examples/files/images/page-generic.jb2 | Bin 0 -> 8595 bytes .../examples/files/images/page-symbol.jb2 | Bin 0 -> 5577 bytes .../demo/examples/files/images/scene-1bit.bmp | Bin 0 -> 24638 bytes .../demo/examples/files/images/scene-24.bmp | Bin 0 -> 57654 bytes .../demo/examples/files/images/scene-32.bmp | Bin 0 -> 76854 bytes .../demo/examples/files/images/scene-8.bmp | Bin 0 -> 20278 bytes .../examples/files/images/scene-alpha.jp2 | Bin 0 -> 78619 bytes .../examples/files/images/scene-alpha.jxl | Bin 0 -> 35576 bytes .../examples/files/images/scene-alpha.pam | Bin 0 -> 49220 bytes .../examples/files/images/scene-alpha.png | Bin 0 -> 48816 bytes .../examples/files/images/scene-alpha.qoi | Bin 0 -> 90838 bytes .../examples/files/images/scene-alpha.webp | Bin 0 -> 18784 bytes .../examples/files/images/scene-ascii.ppm | 40 + .../demo/examples/files/images/scene-bc1.dds | Bin 0 -> 32904 bytes .../demo/examples/files/images/scene-bc3.dds | Bin 0 -> 65680 bytes .../files/images/scene-cmyk-packbits.tif | Bin 0 -> 49730 bytes .../demo/examples/files/images/scene-cmyk.jpg | Bin 0 -> 52705 bytes .../demo/examples/files/images/scene-grey.jp2 | Bin 0 -> 19629 bytes .../demo/examples/files/images/scene-grey.jpg | Bin 0 -> 17273 bytes .../examples/files/images/scene-grey16.pgm | 4 + .../examples/files/images/scene-indexed.png | Bin 0 -> 5152 bytes .../demo/examples/files/images/scene-jpeg.tif | Bin 0 -> 57205 bytes .../examples/files/images/scene-lossless.jxl | Bin 0 -> 26223 bytes .../examples/files/images/scene-lossless.webp | Bin 0 -> 18644 bytes .../examples/files/images/scene-lossy.jxl | Bin 0 -> 23664 bytes .../demo/examples/files/images/scene-lzw.tif | Bin 0 -> 96668 bytes .../files/images/scene-orientation6.jpg | Bin 0 -> 22193 bytes .../examples/files/images/scene-pages.tif | Bin 0 -> 84896 bytes .../files/images/scene-progressive.jpg | Bin 0 -> 20456 bytes .../demo/examples/files/images/scene-rle8.bmp | Bin 0 -> 10838 bytes .../demo/examples/files/images/scene.ff | Bin 0 -> 55312 bytes .../demo/examples/files/images/scene.gif | Bin 0 -> 25539 bytes .../demo/examples/files/images/scene.j2k | Bin 0 -> 49050 bytes .../demo/examples/files/images/scene.jp2 | Bin 0 -> 81825 bytes .../demo/examples/files/images/scene.jpg | Bin 0 -> 24631 bytes .../demo/examples/files/images/scene.pbm | Bin 0 -> 6155 bytes .../demo/examples/files/images/scene.png | Bin 0 -> 26756 bytes .../demo/examples/files/images/scene.ppm | 23 + .../demo/examples/files/images/scene.qoi | Bin 0 -> 38960 bytes .../demo/examples/files/images/scene.webp | Bin 0 -> 9040 bytes .../examples/files/images/sunset-flat.hdr | 7 + .../demo/examples/files/images/sunset.hdr | Bin 0 -> 55026 bytes .../demo/examples/files/media/canon.flac | Bin 0 -> 74904 bytes .../demo/examples/files/media/canon.m4a | Bin 0 -> 25066 bytes .../demo/examples/files/media/canon.mp3 | Bin 0 -> 25920 bytes .../demo/examples/files/media/canon.ogg | Bin 0 -> 14618 bytes .../demo/examples/files/media/canon.opus | Bin 0 -> 16274 bytes .../demo/examples/files/media/canon.wav | Bin 0 -> 134444 bytes .../demo/examples/files/media/testcard.mp4 | Bin 0 -> 41946 bytes .../demo/examples/files/media/testcard.webm | Bin 0 -> 42004 bytes .../demo/examples/files/models/arm.glb | Bin 0 -> 29036 bytes .../examples/files/models/bracket-ascii.stl | 254 + .../demo/examples/files/models/gear.stl | Bin 0 -> 43284 bytes .../examples/files/models/icosphere-ascii.ply | 499 + .../demo/examples/files/models/knot.obj | 2944 ++ .../demo/examples/files/models/table.gltf | 1 + .../demo/examples/files/models/terrain.ply | Bin 0 -> 119979 bytes .../demo/examples/files/office/deck.pptx | Bin 0 -> 30490 bytes .../demo/examples/files/office/handbook.docx | Bin 0 -> 25180 bytes .../demo/examples/files/office/workbook.xlsx | Bin 0 -> 23363 bytes .../demo/examples/files/pdf/annotations.pdf | Bin 0 -> 6245 bytes .../examples/files/pdf/application-form.pdf | Bin 0 -> 19127 bytes .../demo/examples/files/pdf/field-report.pdf | Bin 0 -> 107001 bytes .../examples/files/pdf/handbook-tagged.pdf | Bin 0 -> 192239 bytes .../demo/examples/files/pdf/scan-mixed.pdf | Bin 0 -> 206092 bytes .../demo/examples/files/pdf/vector-art.pdf | Bin 0 -> 4368 bytes .../demo/examples/files/text/bits.c | 51 + .../demo/examples/files/text/changes.diff | 31 + .../demo/examples/files/text/config.yaml | 28 + .../demo/examples/files/text/deploy.sh | 42 + .../demo/examples/files/text/feed.xml | 27 + .../demo/examples/files/text/field-guide.md | 124 + .../demo/examples/files/text/field-notes.txt | 30 + .../demo/examples/files/text/harbour-log.csv | 332 + .../demo/examples/files/text/life.py | 34 + .../demo/examples/files/text/lru.rs | 57 + .../demo/examples/files/text/page.html | 28 + .../demo/examples/files/text/places.kml | 22 + .../demo/examples/files/text/prix.csv | 7 + .../demo/examples/files/text/pyproject.toml | 22 + .../demo/examples/files/text/recipes.json | 133 + .../demo/examples/files/text/report.sql | 43 + .../demo/examples/files/text/route.gpx | 71 + .../demo/examples/files/text/server.log | 120 + .../demo/examples/files/text/style.css | 50 + .../demo/examples/files/text/subtitles.srt | 23 + .../demo/examples/files/text/tides.tsv | 29 + .../demo/examples/files/text/tokens.ts | 54 + .../demo/examples/generate/catalog.json | 376 + .../demo/examples/generate/make-archives.py | 279 + .../examples/generate/make-attributions.mjs | 140 + .../demo/examples/generate/make-cad.py | 970 + .../demo/examples/generate/make-ifc.py | 282 + .../demo/examples/generate/make-images.py | 593 + .../demo/examples/generate/make-manifest.mjs | 82 + .../demo/examples/generate/make-media.py | 82 + .../demo/examples/generate/make-models.py | 458 + .../demo/examples/generate/make-office.py | 801 + .../demo/examples/generate/make-pdf.py | 643 + .../demo/examples/generate/make-text.py | 800 + .../examples/generate/verify-evidence.mjs | 96 + .../exav-viewer/demo/examples/verify-open.mjs | 205 + crates/exav-viewer/demo/headless.html | 32 + crates/exav-viewer/demo/src/App.tsx | 145 +- crates/exav-viewer/demo/src/ExportImage.tsx | 184 + crates/exav-viewer/demo/src/demo.css | 112 +- crates/exav-viewer/demo/src/headless.css | 110 + crates/exav-viewer/demo/src/headless.ts | 234 + crates/exav-viewer/demo/src/showcase.ts | 33 +- crates/exav-viewer/demo/vite.config.ts | 38 +- crates/exav-viewer/e2e/cad-raster.spec.ts | 684 + crates/exav-viewer/e2e/cad.spec.ts | 305 + crates/exav-viewer/e2e/converted.spec.ts | 134 + crates/exav-viewer/e2e/csp.ts | 18 +- crates/exav-viewer/e2e/delivery-setup.ts | 2 + crates/exav-viewer/e2e/delivery.spec.ts | 39 +- crates/exav-viewer/e2e/demo.spec.ts | 511 +- crates/exav-viewer/e2e/docs.spec.ts | 53 + crates/exav-viewer/e2e/external.spec.ts | 99 + .../e2e/fixtures/fake-converter.js | 71 + .../exav-viewer/e2e/fixtures/make-samples.mjs | 235 +- crates/exav-viewer/e2e/fonts.conf | 8 + crates/exav-viewer/e2e/frame-protocol.spec.ts | 223 + crates/exav-viewer/e2e/frame.spec.ts | 18 +- crates/exav-viewer/e2e/headless.spec.ts | 126 + crates/exav-viewer/e2e/lifetime.spec.ts | 6 +- crates/exav-viewer/e2e/meshes.spec.ts | 117 + crates/exav-viewer/e2e/model-raster.spec.ts | 642 + crates/exav-viewer/e2e/model.spec.ts | 4 +- crates/exav-viewer/e2e/office-raster.spec.ts | 533 + crates/exav-viewer/e2e/pdf-decoders.spec.ts | 8 +- crates/exav-viewer/e2e/pinch.spec.ts | 75 +- crates/exav-viewer/e2e/progressive.spec.ts | 44 + crates/exav-viewer/e2e/raster-frame.spec.ts | 284 + crates/exav-viewer/e2e/raw.spec.ts | 135 + crates/exav-viewer/e2e/selection.spec.ts | 12 +- crates/exav-viewer/e2e/sheets.spec.ts | 84 + crates/exav-viewer/e2e/slides.spec.ts | 131 + crates/exav-viewer/e2e/tools.spec.ts | 54 + crates/exav-viewer/e2e/zip.ts | 31 + crates/exav-viewer/package-lock.json | 1758 +- crates/exav-viewer/package.json | 53 +- crates/exav-viewer/playwright.config.ts | 24 +- crates/exav-viewer/scripts/build-frame.mjs | 10 +- crates/exav-viewer/scripts/build-wasm.sh | 22 +- crates/exav-viewer/scripts/build.mjs | 8 + crates/exav-viewer/scripts/check-package.mjs | 2 +- crates/exav-viewer/scripts/check-sinks.mjs | 18 + .../scripts/check-wasm-imports.mjs | 8 + .../scripts/check-wasm-imports.test.mjs | 2 +- crates/exav-viewer/scripts/sync-examples.mjs | 109 + crates/exav-viewer/src/docs.rs | 59 + crates/exav-viewer/src/lib.rs | 92 +- crates/exav-viewer/src/sheets.rs | 51 + crates/exav-viewer/src/slides.rs | 72 + crates/exav-viewer/ts/all.ts | 7 +- crates/exav-viewer/ts/archive/index.ts | 6 +- crates/exav-viewer/ts/archive/names.test.ts | 20 +- crates/exav-viewer/ts/archive/names.ts | 9 + crates/exav-viewer/ts/archive/renderer.ts | 6 +- crates/exav-viewer/ts/cad/cad.worker.ts | 16 +- crates/exav-viewer/ts/cad/export.ts | 159 + crates/exav-viewer/ts/cad/plugin.ts | 22 +- crates/exav-viewer/ts/cad/raster.test.ts | 367 + crates/exav-viewer/ts/cad/raster.ts | 275 + .../ts/cad/renderer/camera.test.ts | 12 + crates/exav-viewer/ts/cad/renderer/camera.ts | 26 +- crates/exav-viewer/ts/cad/renderer/fonts.ts | 11 + .../ts/cad/renderer/glyph-atlas.ts | 86 +- .../exav-viewer/ts/cad/renderer/renderer.ts | 614 +- .../exav-viewer/ts/cad/renderer/sdf.test.ts | 95 + crates/exav-viewer/ts/cad/renderer/sdf.ts | 128 + crates/exav-viewer/ts/cad/renderer/shaders.ts | 116 +- .../ts/cad/renderer/text-layout.test.ts | 88 +- .../ts/cad/renderer/text-layout.ts | 126 +- .../ts/cad/renderer/text-resolution.test.ts | 23 +- .../ts/cad/renderer/text-resolution.ts | 17 +- .../exav-viewer/ts/cad/renderer/tiles.test.ts | 26 +- crates/exav-viewer/ts/cad/renderer/tiles.ts | 25 + crates/exav-viewer/ts/cad/types.ts | 54 +- crates/exav-viewer/ts/cad/view.ts | 252 +- crates/exav-viewer/ts/cad/warnings.test.ts | 42 + crates/exav-viewer/ts/cad/warnings.ts | 19 + .../exav-viewer/ts/cad/zoom-gesture.test.ts | 130 + crates/exav-viewer/ts/cad/zoom-gesture.ts | 104 + crates/exav-viewer/ts/core/assets.ts | 6 + crates/exav-viewer/ts/core/detect.test.ts | 312 +- crates/exav-viewer/ts/core/formats.ts | 470 +- crates/exav-viewer/ts/core/index.ts | 22 +- crates/exav-viewer/ts/core/messages.test.ts | 27 + crates/exav-viewer/ts/core/messages.ts | 505 + crates/exav-viewer/ts/core/pictures.test.ts | 210 + crates/exav-viewer/ts/core/pictures.ts | 325 + crates/exav-viewer/ts/core/print.ts | 94 + crates/exav-viewer/ts/core/raster.test.ts | 266 + crates/exav-viewer/ts/core/raster.ts | 215 + crates/exav-viewer/ts/core/registry.test.ts | 72 + crates/exav-viewer/ts/core/store.test.ts | 59 +- crates/exav-viewer/ts/core/store.ts | 33 + crates/exav-viewer/ts/core/theme.ts | 116 + crates/exav-viewer/ts/core/types.ts | 319 +- crates/exav-viewer/ts/core/viewer.test.ts | 55 + crates/exav-viewer/ts/core/viewer.ts | 34 +- crates/exav-viewer/ts/frame/app/frame.css | 2 +- crates/exav-viewer/ts/frame/app/main.ts | 280 +- crates/exav-viewer/ts/frame/app/ranges.ts | 14 +- crates/exav-viewer/ts/frame/host.test.ts | 534 +- crates/exav-viewer/ts/frame/index.ts | 398 +- crates/exav-viewer/ts/frame/policy.ts | 14 +- crates/exav-viewer/ts/frame/protocol.test.ts | 283 +- crates/exav-viewer/ts/frame/protocol.ts | 339 +- crates/exav-viewer/ts/ifc/index.ts | 24 +- crates/exav-viewer/ts/ifc/renderer.ts | 44 +- crates/exav-viewer/ts/image/index.ts | 13 +- crates/exav-viewer/ts/image/renderer.test.ts | 66 +- crates/exav-viewer/ts/image/renderer.ts | 130 +- crates/exav-viewer/ts/image/surface.test.ts | 6 +- crates/exav-viewer/ts/image/surface.ts | 18 +- crates/exav-viewer/ts/image/svg.test.ts | 23 + crates/exav-viewer/ts/image/svg.ts | 57 + crates/exav-viewer/ts/media/index.ts | 184 +- crates/exav-viewer/ts/media/media.test.ts | 132 + crates/exav-viewer/ts/model/camera.test.ts | 180 + crates/exav-viewer/ts/model/camera.ts | 234 + crates/exav-viewer/ts/model/engine.test.ts | 58 +- crates/exav-viewer/ts/model/engine.ts | 20 +- crates/exav-viewer/ts/model/index.ts | 65 +- crates/exav-viewer/ts/model/model.worker.ts | 12 +- crates/exav-viewer/ts/model/raster.test.ts | 613 + crates/exav-viewer/ts/model/raster.ts | 467 + crates/exav-viewer/ts/model/stl.ts | 65 +- crates/exav-viewer/ts/model/storeys.test.ts | 89 + crates/exav-viewer/ts/model/storeys.ts | 56 + crates/exav-viewer/ts/office/convert.test.ts | 227 + crates/exav-viewer/ts/office/convert.ts | 188 + .../ts/office/docs-convert.test.ts | 70 + crates/exav-viewer/ts/office/docs.test.ts | 244 + crates/exav-viewer/ts/office/docs.ts | 135 + crates/exav-viewer/ts/office/docs.worker.ts | 22 + crates/exav-viewer/ts/office/export.test.ts | 273 + crates/exav-viewer/ts/office/export.ts | 205 + crates/exav-viewer/ts/office/external.ts | 136 + crates/exav-viewer/ts/office/index.ts | 169 +- .../exav-viewer/ts/office/progressive.test.ts | 51 + crates/exav-viewer/ts/office/progressive.ts | 58 + crates/exav-viewer/ts/office/raster.test.ts | 478 + crates/exav-viewer/ts/office/raster.ts | 402 + .../ts/office/renderer-raster.test.ts | 433 + crates/exav-viewer/ts/office/renderer.test.ts | 137 + crates/exav-viewer/ts/office/renderer.ts | 348 +- crates/exav-viewer/ts/office/route.ts | 39 + .../exav-viewer/ts/office/selection.test.ts | 10 +- crates/exav-viewer/ts/office/selection.ts | 2 +- crates/exav-viewer/ts/office/sheets.test.ts | 205 + crates/exav-viewer/ts/office/sheets.ts | 138 + crates/exav-viewer/ts/office/sheets.worker.ts | 22 + crates/exav-viewer/ts/office/slides.test.ts | 250 + crates/exav-viewer/ts/office/slides.ts | 193 + crates/exav-viewer/ts/office/slides.worker.ts | 22 + crates/exav-viewer/ts/office/table.test.ts | 122 + crates/exav-viewer/ts/office/table.ts | 174 + crates/exav-viewer/ts/office/viewport.test.ts | 119 + crates/exav-viewer/ts/office/viewport.ts | 82 + crates/exav-viewer/ts/pdf/engine.ts | 38 +- crates/exav-viewer/ts/pdf/index.ts | 2 +- crates/exav-viewer/ts/pdf/reader.test.ts | 79 +- crates/exav-viewer/ts/pdf/reader.ts | 72 +- crates/exav-viewer/ts/pdf/renderer.ts | 33 +- .../exav-viewer/ts/react/ViewerBody.test.tsx | 294 +- crates/exav-viewer/ts/react/ViewerBody.tsx | 41 +- .../ts/react/ViewerDialog.test.tsx | 20 +- crates/exav-viewer/ts/react/ViewerDialog.tsx | 38 +- crates/exav-viewer/ts/react/hooks.ts | 64 +- crates/exav-viewer/ts/react/icons.tsx | 15 +- crates/exav-viewer/ts/react/index.ts | 33 +- crates/exav-viewer/ts/react/messages.ts | 173 +- crates/exav-viewer/ts/react/overlay.tsx | 2 +- crates/exav-viewer/ts/react/parts.tsx | 273 +- crates/exav-viewer/ts/react/styles.css | 803 +- crates/exav-viewer/ts/test/fake-canvas.ts | 63 + crates/exav-viewer/ts/test/markdown.ts | 17 + crates/exav-viewer/ts/test/zip-members.ts | 29 + crates/exav-viewer/ts/text/decode.test.ts | 134 + crates/exav-viewer/ts/text/decode.ts | 139 + crates/exav-viewer/ts/text/index.ts | 25 + crates/exav-viewer/ts/text/json.test.ts | 95 + crates/exav-viewer/ts/text/json.ts | 161 + crates/exav-viewer/ts/text/lines.ts | 41 + crates/exav-viewer/ts/text/markdown-engine.ts | 22 + crates/exav-viewer/ts/text/markdown-view.ts | 151 + crates/exav-viewer/ts/text/markdown.test.ts | 241 + crates/exav-viewer/ts/text/markdown.ts | 69 + crates/exav-viewer/ts/text/markdown.worker.ts | 13 + crates/exav-viewer/ts/text/renderer.test.ts | 332 + crates/exav-viewer/ts/text/renderer.ts | 204 + crates/exav-viewer/ts/text/xml.test.ts | 109 + crates/exav-viewer/ts/text/xml.ts | 225 + crates/exav-viewer/ts/vite/assets.test.ts | 63 +- crates/exav-viewer/ts/vite/assets.ts | 77 +- crates/exav-viewer/ts/vite/cli.ts | 18 +- crates/exav-viewer/ts/vite/index.ts | 17 +- .../zenjxl-decoder/.cargo_vcs_info.json | 6 + .../vendor/zenjxl-decoder/Cargo.lock | 1815 ++ .../vendor/zenjxl-decoder/Cargo.toml | 180 + .../exav-viewer/vendor/zenjxl-decoder/EXAV.md | 21 + .../exav-viewer/vendor/zenjxl-decoder/LICENSE | 27 + .../vendor/zenjxl-decoder/README.md | 247 + .../vendor/zenjxl-decoder/src/api/color.rs | 2799 ++ .../zenjxl-decoder/src/api/convenience.rs | 428 + .../zenjxl-decoder/src/api/data_types.rs | 297 + .../vendor/zenjxl-decoder/src/api/decoder.rs | 1929 ++ .../src/api/inner/box_parser.rs | 473 + .../src/api/inner/codestream_parser/mod.rs | 545 + .../inner/codestream_parser/non_section.rs | 441 + .../api/inner/codestream_parser/sections.rs | 446 + .../zenjxl-decoder/src/api/inner/mod.rs | 253 + .../zenjxl-decoder/src/api/inner/process.rs | 149 + .../vendor/zenjxl-decoder/src/api/input.rs | 82 + .../vendor/zenjxl-decoder/src/api/mod.rs | 150 + .../zenjxl-decoder/src/api/moxcms_wrapper.rs | 281 + .../vendor/zenjxl-decoder/src/api/options.rs | 180 + .../zenjxl-decoder/src/api/signature.rs | 162 + .../zenjxl-decoder/src/api/xyb_constants.rs | 86 + .../vendor/zenjxl-decoder/src/bit_reader.rs | 347 + .../vendor/zenjxl-decoder/src/color/mod.rs | 6 + .../vendor/zenjxl-decoder/src/color/tf.rs | 819 + .../src/container/box_header.rs | 114 + .../src/container/frame_index.rs | 236 + .../zenjxl-decoder/src/container/gain_map.rs | 452 + .../zenjxl-decoder/src/container/mod.rs | 202 + .../zenjxl-decoder/src/container/parse.rs | 273 + .../zenjxl-decoder/src/entropy_coding/ans.rs | 503 + .../src/entropy_coding/context_map.rs | 77 + .../src/entropy_coding/decode.rs | 817 + .../src/entropy_coding/huffman.rs | 621 + .../src/entropy_coding/hybrid_uint.rs | 144 + .../zenjxl-decoder/src/entropy_coding/mod.rs | 10 + .../vendor/zenjxl-decoder/src/error.rs | 319 + .../zenjxl-decoder/src/features/blending.rs | 806 + .../vendor/zenjxl-decoder/src/features/epf.rs | 84 + .../vendor/zenjxl-decoder/src/features/mod.rs | 10 + .../zenjxl-decoder/src/features/noise.rs | 40 + .../zenjxl-decoder/src/features/patches.rs | 2056 ++ .../zenjxl-decoder/src/features/spline.rs | 2041 ++ .../src/frame/adaptive_lf_smoothing.rs | 198 + .../src/frame/block_context_map.rs | 153 + .../zenjxl-decoder/src/frame/coeff_order.rs | 187 + .../src/frame/color_correlation_map.rs | 91 + .../vendor/zenjxl-decoder/src/frame/decode.rs | 1482 + .../vendor/zenjxl-decoder/src/frame/group.rs | 755 + .../zenjxl-decoder/src/frame/lf_preview.rs | 384 + .../vendor/zenjxl-decoder/src/frame/mod.rs | 804 + .../src/frame/modular/borrowed_buffers.rs | 50 + .../src/frame/modular/decode/bitstream.rs | 103 + .../src/frame/modular/decode/channel.rs | 217 + .../src/frame/modular/decode/common.rs | 88 + .../src/frame/modular/decode/mod.rs | 12 + .../frame/modular/decode/specialized_trees.rs | 532 + .../zenjxl-decoder/src/frame/modular/mod.rs | 1301 + .../src/frame/modular/predict.rs | 642 + .../src/frame/modular/transforms/apply.rs | 1007 + .../src/frame/modular/transforms/mod.rs | 448 + .../src/frame/modular/transforms/palette.rs | 701 + .../src/frame/modular/transforms/rct.rs | 161 + .../src/frame/modular/transforms/squeeze.rs | 675 + .../zenjxl-decoder/src/frame/modular/tree.rs | 644 + .../zenjxl-decoder/src/frame/quant_weights.rs | 2194 ++ .../zenjxl-decoder/src/frame/quantizer.rs | 77 + .../vendor/zenjxl-decoder/src/frame/render.rs | 2189 ++ .../zenjxl-decoder/src/headers/bit_depth.rs | 104 + .../src/headers/color_encoding.rs | 204 + .../zenjxl-decoder/src/headers/encodings.rs | 415 + .../src/headers/extra_channels.rs | 106 + .../src/headers/frame_header.rs | 978 + .../src/headers/image_metadata.rs | 247 + .../vendor/zenjxl-decoder/src/headers/mod.rs | 82 + .../zenjxl-decoder/src/headers/modular.rs | 166 + .../zenjxl-decoder/src/headers/permutation.rs | 358 + .../vendor/zenjxl-decoder/src/headers/size.rs | 160 + .../vendor/zenjxl-decoder/src/headers/toc.rs | 173 + .../src/headers/transform_data.rs | 342 + .../vendor/zenjxl-decoder/src/icc/header.rs | 51 + .../vendor/zenjxl-decoder/src/icc/mod.rs | 260 + .../vendor/zenjxl-decoder/src/icc/stream.rs | 118 + .../vendor/zenjxl-decoder/src/icc/tag.rs | 251 + .../zenjxl-decoder/src/image/data_type.rs | 138 + .../zenjxl-decoder/src/image/internal.rs | 340 + .../vendor/zenjxl-decoder/src/image/mod.rs | 20 + .../zenjxl-decoder/src/image/output_buffer.rs | 555 + .../vendor/zenjxl-decoder/src/image/raw.rs | 415 + .../vendor/zenjxl-decoder/src/image/rect.rs | 61 + .../vendor/zenjxl-decoder/src/image/test.rs | 208 + .../vendor/zenjxl-decoder/src/image/typed.rs | 442 + .../vendor/zenjxl-decoder/src/jpeg/data.rs | 168 + .../vendor/zenjxl-decoder/src/jpeg/jbrd.rs | 486 + .../vendor/zenjxl-decoder/src/jpeg/mod.rs | 95 + .../vendor/zenjxl-decoder/src/jpeg/writer.rs | 1039 + .../vendor/zenjxl-decoder/src/lib.rs | 89 + .../src/render/buffer_splitter.rs | 238 + .../zenjxl-decoder/src/render/builder.rs | 318 + .../zenjxl-decoder/src/render/channels.rs | 162 + .../zenjxl-decoder/src/render/internal.rs | 259 + .../low_memory_pipeline/group_scheduler.rs | 778 + .../src/render/low_memory_pipeline/helpers.rs | 86 + .../src/render/low_memory_pipeline/mod.rs | 977 + .../low_memory_pipeline/render_group.rs | 485 + .../render/low_memory_pipeline/row_buffers.rs | 128 + .../render/low_memory_pipeline/run_stage.rs | 156 + .../low_memory_pipeline/save/identity.rs | 274 + .../render/low_memory_pipeline/save/mod.rs | 143 + .../vendor/zenjxl-decoder/src/render/mod.rs | 179 + .../vendor/zenjxl-decoder/src/render/save.rs | 100 + .../zenjxl-decoder/src/render/simd_utils.rs | 238 + .../src/render/simple_pipeline/extend.rs | 94 + .../src/render/simple_pipeline/mod.rs | 244 + .../src/render/simple_pipeline/run_stage.rs | 204 + .../src/render/simple_pipeline/save.rs | 242 + .../zenjxl-decoder/src/render/stages/black.rs | 208 + .../src/render/stages/blending.rs | 180 + .../src/render/stages/chroma_upsample.rs | 236 + .../zenjxl-decoder/src/render/stages/cms.rs | 495 + .../src/render/stages/cms_cmyk.rs | 119 + .../src/render/stages/convert.rs | 746 + .../src/render/stages/epf/common.rs | 78 + .../src/render/stages/epf/epf0.rs | 268 + .../src/render/stages/epf/epf1.rs | 207 + .../src/render/stages/epf/epf2.rs | 171 + .../src/render/stages/epf/mod.rs | 16 + .../src/render/stages/epf/test.rs | 45 + .../src/render/stages/extend.rs | 117 + .../src/render/stages/from_linear.rs | 416 + .../src/render/stages/gaborish.rs | 145 + .../zenjxl-decoder/src/render/stages/mod.rs | 53 + .../src/render/stages/nearest_neighbor.rs | 97 + .../zenjxl-decoder/src/render/stages/noise.rs | 339 + .../src/render/stages/patches.rs | 99 + .../src/render/stages/premultiply_alpha.rs | 144 + .../src/render/stages/splines.rs | 174 + .../zenjxl-decoder/src/render/stages/spot.rs | 115 + .../src/render/stages/to_linear.rs | 233 + .../src/render/stages/tone_mapping.rs | 491 + .../src/render/stages/unpremultiply_alpha.rs | 204 + .../src/render/stages/upsample.rs | 868 + .../zenjxl-decoder/src/render/stages/xyb.rs | 683 + .../zenjxl-decoder/src/render/stages/ycbcr.rs | 153 + .../vendor/zenjxl-decoder/src/render/test.rs | 241 + .../zenjxl-decoder/src/transforms/idct16.rs | 355 + .../zenjxl-decoder/src/transforms/idct2.rs | 59 + .../zenjxl-decoder/src/transforms/idct2d.rs | 427 + .../zenjxl-decoder/src/transforms/idct32.rs | 796 + .../zenjxl-decoder/src/transforms/idct4.rs | 104 + .../zenjxl-decoder/src/transforms/idct8.rs | 181 + .../src/transforms/idct_large.rs | 589 + .../zenjxl-decoder/src/transforms/mod.rs | 41 + .../src/transforms/reinterpreting_dct16.rs | 385 + .../src/transforms/reinterpreting_dct2.rs | 39 + .../src/transforms/reinterpreting_dct2d.rs | 645 + .../src/transforms/reinterpreting_dct32.rs | 791 + .../src/transforms/reinterpreting_dct4.rs | 108 + .../src/transforms/reinterpreting_dct8.rs | 193 + .../zenjxl-decoder/src/transforms/tests.rs | 490 + .../src/transforms/transform.rs | 662 + .../src/transforms/transform_map.rs | 116 + .../vendor/zenjxl-decoder/src/util/bits.rs | 23 + .../zenjxl-decoder/src/util/cacheline.rs | 52 + .../vendor/zenjxl-decoder/src/util/clock.rs | 32 + .../zenjxl-decoder/src/util/concat_slice.rs | 125 + .../zenjxl-decoder/src/util/fast_math.rs | 245 + .../vendor/zenjxl-decoder/src/util/float16.rs | 311 + .../vendor/zenjxl-decoder/src/util/linalg.rs | 141 + .../vendor/zenjxl-decoder/src/util/log2.rs | 85 + .../zenjxl-decoder/src/util/memory_tracker.rs | 253 + .../vendor/zenjxl-decoder/src/util/mirror.rs | 18 + .../vendor/zenjxl-decoder/src/util/mod.rs | 46 + .../vendor/zenjxl-decoder/src/util/ndarray.rs | 24 + .../zenjxl-decoder/src/util/profiling.rs | 249 + .../zenjxl-decoder/src/util/rational_poly.rs | 35 + .../src/util/shift_right_ceil.rs | 41 + .../vendor/zenjxl-decoder/src/util/test.rs | 420 + .../src/util/tracing_wrappers.rs | 26 + .../zenjxl-decoder/src/util/vec_helpers.rs | 142 + .../src/util/xorshift128plus.rs | 734 + crates/exav-x86/tools/gen-tables/Cargo.lock | 4 +- crates/exav/Cargo.toml | 11 +- crates/exav/src/daemon.rs | 37 +- crates/exav/src/main.rs | 154 +- crates/exav/tests/daemon_client.rs | 24 + crates/exav/tests/fixtures/split/e.z01 | Bin 0 -> 65536 bytes crates/exav/tests/fixtures/split/e.zip | Bin 0 -> 4628 bytes crates/exav/tests/office_text.rs | 106 + crates/exav/tests/split_volumes.rs | 266 + deny.toml | 6 + docs/BACKLOG.md | 324 +- docs/DEPENDENCIES.md | 18 + docs/DIFFTEST_TRIAGE.md | 323 + docs/DIFF_TESTING.md | 4 + docs/FUZZING.md | 125 +- docs/GAPS.md | 21 +- docs/PDF-RENDERER-EVALUATION.md | 122 + docs/STREAMING_FORMATS.md | 62 +- docs/WASM.md | 2 +- formats/registry.json | 162 + fuzz/Cargo.lock | 743 +- fuzz/Cargo.toml | 148 +- fuzz/dicts/markdown.dict | 51 + fuzz/exav-cli/Cargo.toml | 6 +- fuzz/fuzz_targets/analyze.rs | 1 + fuzz/fuzz_targets/docs_check.rs | 55 + fuzz/fuzz_targets/docs_doc.rs | 28 + fuzz/fuzz_targets/docs_odt.rs | 20 + fuzz/fuzz_targets/docs_pages.rs | 32 + fuzz/fuzz_targets/docs_rtf.rs | 15 + fuzz/fuzz_targets/docs_wml.rs | 14 + fuzz/fuzz_targets/full_pipeline.rs | 1 + fuzz/fuzz_targets/imagehash.rs | 8 +- fuzz/fuzz_targets/markdown.rs | 83 + fuzz/fuzz_targets/mesh.rs | 26 + fuzz/fuzz_targets/raw_preview.rs | 18 + fuzz/fuzz_targets/seekable.rs | 1 + fuzz/fuzz_targets/sheets_check.rs | 44 + fuzz/fuzz_targets/sheets_numbers.rs | 44 + fuzz/fuzz_targets/sheets_ods.rs | 23 + fuzz/fuzz_targets/sheets_text.rs | 25 + fuzz/fuzz_targets/sheets_xls.rs | 18 + fuzz/fuzz_targets/slides_check.rs | 64 + fuzz/fuzz_targets/slides_keynote.rs | 35 + fuzz/fuzz_targets/slides_odg.rs | 20 + fuzz/fuzz_targets/slides_odp.rs | 21 + fuzz/fuzz_targets/slides_ppt.rs | 32 + fuzz/fuzz_targets/slides_vsdx.rs | 15 + fuzz/fuzz_targets/unpack.rs | 1 + fuzz/fuzz_targets/vdisk.rs | 118 + fuzz/fuzz_targets/visible_text.rs | 65 + fuzz/seeds/docs_doc/word6.cfb | Bin 0 -> 5632 bytes fuzz/seeds/sheets_numbers/index09.xml | 25 + fuzz/seeds/slides_keynote/keynote1.apxl | 35 + fuzz/seeds/slides_keynote/keynote2.apxl | 16 + fuzz/seeds/slides_ppt/powerpoint95.bin | Bin 0 -> 727 bytes fuzz/seeds/vdisk/qcow2.bin | Bin 0 -> 5121 bytes fuzz/seeds/vdisk/qcow2_deflate.bin | Bin 0 -> 3073 bytes fuzz/seeds/vdisk/qcow2_zstd.bin | Bin 0 -> 3073 bytes fuzz/seeds/vdisk/vhd.bin | Bin 0 -> 16897 bytes fuzz/seeds/vdisk/vmdk_sparse.bin | Bin 0 -> 15361 bytes fuzz/seeds/vdisk/vmdk_stream.bin | Bin 0 -> 2561 bytes fuzz/support/src/lib.rs | 1 + fuzz/support/src/panics.rs | 65 + scripts/formats.mjs | 238 + scripts/fuzz-campaign.sh | 3 +- scripts/fuzz-seeds-synth.py | 31 + scripts/fuzz-seeds.sh | 197 +- scripts/pdf-compare/README.md | 19 + scripts/pdf-compare/compare3.py | 128 + scripts/pdf-compare/hayro-eval/Cargo.toml | 13 + scripts/pdf-compare/hayro-eval/src/main.rs | 54 + scripts/pdf-compare/hayro-wasm.mjs | 77 + scripts/pdf-compare/hayro-wasm/Cargo.toml | 24 + scripts/pdf-compare/hayro-wasm/src/lib.rs | 27 + scripts/pdf-compare/pdfjs.mjs | 100 + scripts/pdf-compare/run_hayro.py | 48 + scripts/pdf-compare/run_poppler.py | 38 + scripts/release.sh | 3 +- scripts/test-www.sh | 3 + www/astro.config.mjs | 22 +- www/package-lock.json | 3924 +-- www/package.json | 10 +- www/public/og.png | Bin 0 -> 80985 bytes www/public/robots.txt | 4 + www/scripts/make-og-image.py | 114 + www/src/content/docs/index.mdx | 9 +- www/src/content/docs/project/architecture.md | 66 +- www/src/content/docs/project/contributing.md | 7 +- www/src/content/docs/project/dependencies.md | 91 +- www/src/content/docs/project/formats.md | 184 + www/src/content/docs/project/license.md | 8 + www/src/content/docs/project/roadmap.md | 90 +- .../docs/scanner/concepts/how-it-works.md | 7 + .../content/docs/scanner/concepts/quirks.md | 5 +- .../scanner/getting-started/installation.mdx | 4 +- .../content/docs/scanner/guides/daemon.mdx | 17 +- www/src/content/docs/scanner/reference/cli.md | 5 +- .../reference/comparison-with-clamav.md | 23 +- .../docs/scanner/reference/feature-flags.md | 5 +- .../content/docs/scanner/reference/formats.md | 36 +- www/src/content/docs/subprojects/exav-core.md | 9 +- www/src/content/docs/subprojects/exav-grep.md | 42 + .../content/docs/subprojects/exav-render.md | 382 +- www/src/content/docs/subprojects/index.md | 4 +- www/src/content/docs/unpack/formats.md | 3 +- www/src/content/docs/unpack/how-it-works.md | 3 + www/src/content/docs/unpack/rust.md | 5 + www/src/content/docs/viewer/api.md | 206 +- www/src/content/docs/viewer/examples.md | 363 + www/src/content/docs/viewer/index.md | 158 +- www/src/content/docs/viewer/integration.md | 212 +- www/src/content/docs/viewer/react.md | 73 +- www/src/content/docs/viewer/security.md | 92 +- 1161 files changed, 373539 insertions(+), 8751 deletions(-) create mode 100644 crates/exav-core/tests/fixtures/fuzz/yara_nested_parens.yar create mode 100644 crates/exav-core/tests/fixtures/volumes/set4.r00 create mode 100644 crates/exav-core/tests/fixtures/volumes/set4.r01 create mode 100644 crates/exav-core/tests/fixtures/volumes/set4.rar create mode 100644 crates/exav-core/tests/fixtures/volumes/set5.part1.rar create mode 100644 crates/exav-core/tests/fixtures/volumes/set5.part2.rar create mode 100644 crates/exav-core/tests/fixtures/volumes/set5.part3.rar create mode 100644 crates/exav-core/tests/fixtures/volumes/split.z01 create mode 100644 crates/exav-core/tests/fixtures/volumes/split.zip create mode 100644 crates/exav-core/tests/suites/exec_offsets.rs create mode 100644 crates/exav-core/tests/suites/format_registry.rs create mode 100644 crates/exav-core/tests/suites/split_sets.rs create mode 100644 crates/exav-core/tests/suites/visible_text.rs create mode 100644 crates/exav-core/tests/suites/visible_text_office.rs create mode 100644 crates/exav-grep/tests/text.rs create mode 100644 crates/exav-grep/tests/text_office.rs create mode 100644 crates/exav-pe-emu/scripts/gen-exports.py create mode 100644 crates/exav-pe-emu/src/exports/advapi32.txt create mode 100644 crates/exav-pe-emu/src/exports/kernel32.txt create mode 100644 crates/exav-pe-emu/src/exports/ntdll.txt create mode 100644 crates/exav-pe-emu/src/exports/user32.txt create mode 100644 crates/exav-pe-emu/tests/end_to_end.rs create mode 100644 crates/exav-render/NOTICE create mode 100644 crates/exav-render/examples/office_docs_convert.rs create mode 100644 crates/exav-render/examples/office_slides_convert.rs create mode 100644 crates/exav-render/examples/office_text.rs create mode 100644 crates/exav-render/examples/raw_preview.rs create mode 100644 crates/exav-render/scripts/office/common/codepages.py create mode 100644 crates/exav-render/scripts/office/sheets/ftab.json create mode 100644 crates/exav-render/scripts/office/sheets/ftab.py create mode 100644 crates/exav-render/scripts/office/sheets/numbers_fonts.py create mode 100644 crates/exav-render/scripts/office/sheets/numbers_functions.py create mode 100644 crates/exav-render/src/formats/dwg/dimension.rs create mode 100644 crates/exav-render/src/formats/dwg/wide.rs create mode 100644 crates/exav-render/src/formats/gltf.rs create mode 100644 crates/exav-render/src/formats/gltf/json.rs create mode 100644 crates/exav-render/src/formats/ifc/alignment.rs create mode 100644 crates/exav-render/src/formats/ifc/directrix.rs create mode 100644 crates/exav-render/src/formats/ifc/faces.rs create mode 100644 crates/exav-render/src/formats/ifc/grid.rs create mode 100644 crates/exav-render/src/formats/ifc/spline.rs create mode 100644 crates/exav-render/src/formats/image/jxl.rs create mode 100644 crates/exav-render/src/formats/image/raw.rs create mode 100644 crates/exav-render/src/formats/image/raw/bmff.rs create mode 100644 crates/exav-render/src/formats/image/raw/jpeg.rs create mode 100644 crates/exav-render/src/formats/image/raw/tests.rs create mode 100644 crates/exav-render/src/formats/image/raw/tiff.rs create mode 100644 crates/exav-render/src/formats/markdown.rs create mode 100644 crates/exav-render/src/formats/obj.rs create mode 100644 crates/exav-render/src/formats/office/common/base64.rs create mode 100644 crates/exav-render/src/formats/office/common/bytes.rs create mode 100644 crates/exav-render/src/formats/office/common/cfb.rs create mode 100644 crates/exav-render/src/formats/office/common/codepage.rs create mode 100644 crates/exav-render/src/formats/office/common/codepage_tables.rs create mode 100644 crates/exav-render/src/formats/office/common/escher.rs create mode 100644 crates/exav-render/src/formats/office/common/image.rs create mode 100644 crates/exav-render/src/formats/office/common/iwa/mod.rs create mode 100644 crates/exav-render/src/formats/office/common/iwa/proto.rs create mode 100644 crates/exav-render/src/formats/office/common/iwa/snappy.rs create mode 100644 crates/exav-render/src/formats/office/common/mod.rs create mode 100644 crates/exav-render/src/formats/office/common/odf.rs create mode 100644 crates/exav-render/src/formats/office/common/package.rs create mode 100644 crates/exav-render/src/formats/office/common/xml.rs create mode 100644 crates/exav-render/src/formats/office/common/xmlout.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/draw.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/fib.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/lists.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/props.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/sprm.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/story.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/styles.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/tables.rs create mode 100644 crates/exav-render/src/formats/office/docs/doc/word6.rs create mode 100644 crates/exav-render/src/formats/office/docs/docx/body.rs create mode 100644 crates/exav-render/src/formats/office/docs/docx/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/docx/parts.rs create mode 100644 crates/exav-render/src/formats/office/docs/docx/props.rs create mode 100644 crates/exav-render/src/formats/office/docs/error.rs create mode 100644 crates/exav-render/src/formats/office/docs/fields.rs create mode 100644 crates/exav-render/src/formats/office/docs/grid.rs create mode 100644 crates/exav-render/src/formats/office/docs/limits.rs create mode 100644 crates/exav-render/src/formats/office/docs/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/model.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/frame.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/list.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/props.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/table.rs create mode 100644 crates/exav-render/src/formats/office/docs/odt/text.rs create mode 100644 crates/exav-render/src/formats/office/docs/pages/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/rtf/lex.rs create mode 100644 crates/exav-render/src/formats/office/docs/rtf/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/rtf/pict.rs create mode 100644 crates/exav-render/src/formats/office/docs/rtf/words.rs create mode 100644 crates/exav-render/src/formats/office/docs/text.rs create mode 100644 crates/exav-render/src/formats/office/docs/wml/mod.rs create mode 100644 crates/exav-render/src/formats/office/docs/wml/props.rs create mode 100644 crates/exav-render/src/formats/office/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/charts.rs create mode 100644 crates/exav-render/src/formats/office/sheets/css.rs create mode 100644 crates/exav-render/src/formats/office/sheets/error.rs create mode 100644 crates/exav-render/src/formats/office/sheets/fonts.rs create mode 100644 crates/exav-render/src/formats/office/sheets/geom.rs create mode 100644 crates/exav-render/src/formats/office/sheets/html.rs create mode 100644 crates/exav-render/src/formats/office/sheets/html/tests.rs create mode 100644 crates/exav-render/src/formats/office/sheets/limits.rs create mode 100644 crates/exav-render/src/formats/office/sheets/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/model.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/cells.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/fonts.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/format.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/formula.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/functions.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/style.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/table.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/tests.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/v09.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numbers/v09/tests.rs create mode 100644 crates/exav-render/src/formats/office/sheets/numtext.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/cf.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/chart.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/dom.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/draw.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/formula.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/numfmt.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/styles.rs create mode 100644 crates/exav-render/src/formats/office/sheets/ods/table.rs create mode 100644 crates/exav-render/src/formats/office/sheets/r1c1.rs create mode 100644 crates/exav-render/src/formats/office/sheets/sylk.rs create mode 100644 crates/exav-render/src/formats/office/sheets/sylk/tests.rs create mode 100644 crates/exav-render/src/formats/office/sheets/text.rs create mode 100644 crates/exav-render/src/formats/office/sheets/textenc.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/cf.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/chart.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/crypto.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/escher.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/formula.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/ftab.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/sheet.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/stream.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xls/tests.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xlsx/chart.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xlsx/drawing.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xlsx/mod.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xlsx/styles.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xlsx/xml.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xmlss.rs create mode 100644 crates/exav-render/src/formats/office/sheets/xmlss/tests.rs create mode 100644 crates/exav-render/src/formats/office/slides/error.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/apxl.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/apxl1.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/chart.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/shapes.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/table.rs create mode 100644 crates/exav-render/src/formats/office/slides/keynote/text.rs create mode 100644 crates/exav-render/src/formats/office/slides/limits.rs create mode 100644 crates/exav-render/src/formats/office/slides/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/model.rs create mode 100644 crates/exav-render/src/formats/office/slides/odp/geom.rs create mode 100644 crates/exav-render/src/formats/office/slides/odp/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/odp/styles.rs create mode 100644 crates/exav-render/src/formats/office/slides/odp/text.rs create mode 100644 crates/exav-render/src/formats/office/slides/package.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/escher.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/old.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/ole.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/pp9.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/ppt95.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/presets.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/records.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/shapes.rs create mode 100644 crates/exav-render/src/formats/office/slides/ppt/text.rs create mode 100644 crates/exav-render/src/formats/office/slides/pptx/draw.rs create mode 100644 crates/exav-render/src/formats/office/slides/pptx/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/pptx/text.rs create mode 100644 crates/exav-render/src/formats/office/slides/pptx/xml.rs create mode 100644 crates/exav-render/src/formats/office/slides/text.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/convert.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/format.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/formula.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/geom.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/mod.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/resolve.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/sheet.rs create mode 100644 crates/exav-render/src/formats/office/slides/vsdx/text.rs create mode 100644 crates/exav-render/src/formats/office/text.rs create mode 100644 crates/exav-render/src/formats/office/visible.rs create mode 100644 crates/exav-render/src/formats/ply.rs create mode 100644 crates/exav-render/src/formats/soup.rs create mode 100644 crates/exav-render/src/formats/text.rs create mode 100644 crates/exav-render/tests/dwg_fidelity.rs create mode 100644 crates/exav-render/tests/dwg_memory.rs create mode 100644 crates/exav-render/tests/dwg_text.rs create mode 100644 crates/exav-render/tests/fixtures/dwg/circle.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/colours.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/complex_linetypes.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/complex_linetypes.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/dense_hatch.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/dimensions.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/gaps.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/gaps.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/halo.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/not_drawn.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/not_drawn.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/plinegen.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/plinegen.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/sheets.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/solid_only.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/stray.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/text_frames.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/text_many.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/text_mtext.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/text_symbols.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/transparency.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/transparency.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/viewport_text.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/wide.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/xclip-R2000.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/xclip.dwg.gz create mode 100644 crates/exav-render/tests/fixtures/dwg/xclip.dxf.gz create mode 100644 crates/exav-render/tests/fixtures/ifc/alignment.ifc create mode 100644 crates/exav-render/tests/fixtures/ifc/curved_faces.ifc create mode 100644 crates/exav-render/tests/fixtures/ifc/grid.ifc create mode 100644 crates/exav-render/tests/fixtures/ifc/grid_ifc4x3.ifc create mode 100644 crates/exav-render/tests/fixtures/ifc/storeys.ifc create mode 100644 crates/exav-render/tests/fixtures/ifc/sweep_params.ifc create mode 100644 crates/exav-render/tests/fixtures/images/jxl/big-flat.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/grey-lossless.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/mid-grey.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-container.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-lossless.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-lossy-e3.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-lossy-e3.ref.ppm create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-lossy.jxl create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgb-lossy.ref.ppm create mode 100644 crates/exav-render/tests/fixtures/images/jxl/rgba-lossless.jxl create mode 100644 crates/exav-render/tests/fixtures/images/rgb.jxl create mode 100644 crates/exav-render/tests/fixtures/images/rgba.jxl create mode 100644 crates/exav-render/tests/fixtures/office/docs/make.py create mode 100644 crates/exav-render/tests/fixtures/office/docs/sample-2003.xml create mode 100644 crates/exav-render/tests/fixtures/office/docs/sample.doc create mode 100644 crates/exav-render/tests/fixtures/office/docs/sample.fodt create mode 100644 crates/exav-render/tests/fixtures/office/docs/sample.odt create mode 100644 crates/exav-render/tests/fixtures/office/docs/sample.rtf create mode 100644 crates/exav-render/tests/fixtures/office/draw/README.md create mode 100644 crates/exav-render/tests/fixtures/office/draw/make_vsdx.py create mode 100644 crates/exav-render/tests/fixtures/office/draw/sample.fodg create mode 100644 crates/exav-render/tests/fixtures/office/draw/sample.odg create mode 100644 crates/exav-render/tests/fixtures/office/draw/sample.vsdx create mode 100644 crates/exav-render/tests/fixtures/office/draw/themed.vsdx create mode 100644 crates/exav-render/tests/fixtures/office/sheets/README.md create mode 100644 crates/exav-render/tests/fixtures/office/sheets/numbers09.numbers create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample-2003.xml create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.fods create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.html create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.numbers create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.ods create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.slk create mode 100644 crates/exav-render/tests/fixtures/office/sheets/sample.xls create mode 100644 crates/exav-render/tests/fixtures/office/slides/README.md create mode 100644 crates/exav-render/tests/fixtures/office/slides/chart.fodp create mode 100644 crates/exav-render/tests/fixtures/office/slides/chart.odp create mode 100644 crates/exav-render/tests/fixtures/office/slides/chart.ppt create mode 100644 crates/exav-render/tests/fixtures/office/slides/pictures.odp create mode 100644 crates/exav-render/tests/fixtures/office/slides/pictures.ppt create mode 100644 crates/exav-render/tests/fixtures/office/slides/sample.fodp create mode 100644 crates/exav-render/tests/fixtures/office/slides/sample.key create mode 100644 crates/exav-render/tests/fixtures/office/slides/sample.odp create mode 100644 crates/exav-render/tests/fixtures/office/slides/sample.ppt create mode 100755 crates/exav-render/tests/fixtures/office/slides/table.key create mode 100644 crates/exav-render/tests/fixtures/raw/camera-structure-no-preview.tif create mode 100644 crates/exav-render/tests/fixtures/raw/canon-heif.cr3 create mode 100644 crates/exav-render/tests/fixtures/raw/canon-o1.cr3 create mode 100644 crates/exav-render/tests/fixtures/raw/canon-o8.cr3 create mode 100644 crates/exav-render/tests/fixtures/raw/canon.cr2 create mode 100644 crates/exav-render/tests/fixtures/raw/canon.crw create mode 100644 crates/exav-render/tests/fixtures/raw/cfa-tags-only.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-32767.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-32769.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-32772.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-34713.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-65000.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-65535.tif create mode 100644 crates/exav-render/tests/fixtures/raw/compression-99.tif create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o1.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o2.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o3.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o4.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o5.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o6.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o7.raf create mode 100644 crates/exav-render/tests/fixtures/raw/fujifilm-o8.raf create mode 100644 crates/exav-render/tests/fixtures/raw/hasselblad.3fr create mode 100644 crates/exav-render/tests/fixtures/raw/kodak16-be.dcr create mode 100644 crates/exav-render/tests/fixtures/raw/kodak16-le.dcr create mode 100644 crates/exav-render/tests/fixtures/raw/lossy-linear.dng create mode 100644 crates/exav-render/tests/fixtures/raw/make.py create mode 100644 crates/exav-render/tests/fixtures/raw/many-markers.dng create mode 100644 crates/exav-render/tests/fixtures/raw/minolta.mrw create mode 100644 crates/exav-render/tests/fixtures/raw/nikon.nef create mode 100644 crates/exav-render/tests/fixtures/raw/no-preview.dng create mode 100644 crates/exav-render/tests/fixtures/raw/olympus-be.orf create mode 100644 crates/exav-render/tests/fixtures/raw/olympus-rs.orf create mode 100644 crates/exav-render/tests/fixtures/raw/olympus.orf create mode 100644 crates/exav-render/tests/fixtures/raw/orientation-own.dng create mode 100644 crates/exav-render/tests/fixtures/raw/panasonic-no-preview.rw2 create mode 100644 crates/exav-render/tests/fixtures/raw/panasonic.rw2 create mode 100644 crates/exav-render/tests/fixtures/raw/pentax.pef create mode 100644 crates/exav-render/tests/fixtures/raw/phaseone.iiq create mode 100644 crates/exav-render/tests/fixtures/raw/photometric-cfa-only.tif create mode 100644 crates/exav-render/tests/fixtures/raw/plain-with-make.tif create mode 100644 crates/exav-render/tests/fixtures/raw/plain.tif create mode 100644 crates/exav-render/tests/fixtures/raw/preview-grey.dng create mode 100644 crates/exav-render/tests/fixtures/raw/preview-jpeg.dng create mode 100644 crates/exav-render/tests/fixtures/raw/preview-rgb.dng create mode 100644 crates/exav-render/tests/fixtures/raw/private-data-only.tif create mode 100644 crates/exav-render/tests/fixtures/raw/progressive.dng create mode 100644 crates/exav-render/tests/fixtures/raw/samsung.srw create mode 100644 crates/exav-render/tests/fixtures/raw/sigma.x3f create mode 100644 crates/exav-render/tests/fixtures/raw/sony-thumbnail.srf create mode 100644 crates/exav-render/tests/fixtures/raw/sony.arw create mode 100644 crates/exav-render/tests/gltf.rs create mode 100644 crates/exav-render/tests/ifc_geometry.rs create mode 100644 crates/exav-render/tests/ifc_hostile.rs create mode 100644 crates/exav-render/tests/jxl.rs create mode 100644 crates/exav-render/tests/markdown.rs create mode 100644 crates/exav-render/tests/markdown_text.rs create mode 100644 crates/exav-render/tests/mesh_common/mod.rs create mode 100644 crates/exav-render/tests/obj.rs create mode 100644 crates/exav-render/tests/office_common/mod.rs create mode 100644 crates/exav-render/tests/office_docs_doc.rs create mode 100644 crates/exav-render/tests/office_docs_formats.rs create mode 100644 crates/exav-render/tests/office_sheets.rs create mode 100644 crates/exav-render/tests/office_slides.rs create mode 100644 crates/exav-render/tests/office_slides_draw.rs create mode 100644 crates/exav-render/tests/office_slides_keynote_xml.rs create mode 100644 crates/exav-render/tests/office_slides_odp_features.rs create mode 100644 crates/exav-render/tests/office_slides_old_ppt.rs create mode 100644 crates/exav-render/tests/office_slides_vsdx.rs create mode 100644 crates/exav-render/tests/office_text_docs.rs create mode 100644 crates/exav-render/tests/office_text_sheets.rs create mode 100644 crates/exav-render/tests/office_text_slides.rs create mode 100644 crates/exav-render/tests/ply.rs create mode 100644 crates/exav-render/tests/raw.rs create mode 100644 crates/exav-render/tests/text.rs create mode 100644 crates/exav-unpack/src/formats/cab_join.rs create mode 100644 crates/exav-unpack/src/formats/dcl_explode.rs create mode 100644 crates/exav-unpack/src/formats/lzfse.rs create mode 100644 crates/exav-unpack/src/formats/vdisk.rs create mode 100644 crates/exav-unpack/src/formats/volumes.rs create mode 100644 crates/exav-unpack/src/oracle.rs create mode 100644 crates/exav-unpack/src/text/html.rs create mode 100644 crates/exav-unpack/src/text/mod.rs create mode 100644 crates/exav-unpack/src/text/pdf.rs create mode 100644 crates/exav-unpack/src/text/pdf_screen.rs create mode 100644 crates/exav-unpack/src/text/xml.rs create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.deflate.qcow2.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.dynamic.vhd.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.dynamic.vhdx.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.plain.qcow2.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.sparse.vmdk.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.stream.vmdk.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/diskimage/fat4g.zstd.qcow2.gz.xor create mode 100644 crates/exav-unpack/tests/fixtures/dmg/udif_lzfse.dmg create mode 100755 crates/exav-unpack/tests/oracle/build.sh create mode 100644 crates/exav-unpack/tests/oracle/dcl_oracle.c create mode 100644 crates/exav-unpack/tests/oracle/lzfse_oracle.c create mode 100644 crates/exav-unpack/tests/suites/cab_salvage.rs create mode 100644 crates/exav-unpack/tests/suites/cab_volumes.rs create mode 100644 crates/exav-unpack/tests/suites/lzfse.rs create mode 100644 crates/exav-unpack/tests/suites/text.rs create mode 100644 crates/exav-unpack/tests/suites/virtual_disks.rs create mode 100644 crates/exav-viewer/LICENSES/BSD-3-Clause.txt create mode 100644 crates/exav-viewer/demo/examples/NOTICE.md create mode 100644 crates/exav-viewer/demo/examples/check.mjs create mode 100644 crates/exav-viewer/demo/examples/examples.json create mode 100644 crates/exav-viewer/demo/examples/files/archives/encrypted-aes.zip create mode 100644 crates/exav-viewer/demo/examples/files/archives/encrypted-zipcrypto.zip create mode 100644 crates/exav-viewer/demo/examples/files/archives/encrypted.7z create mode 100644 crates/exav-viewer/demo/examples/files/archives/example.jar create mode 100644 crates/exav-viewer/demo/examples/files/archives/exav-example.deb create mode 100644 crates/exav-viewer/demo/examples/files/archives/libexample.a create mode 100644 crates/exav-viewer/demo/examples/files/archives/matryoshka.zip create mode 100644 crates/exav-viewer/demo/examples/files/archives/pages.cbz create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.7z create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.cab create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.cpio create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.iso create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.rar create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.tar create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.tar.bz2 create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.tar.gz create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.tar.xz create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.tar.zst create mode 100644 crates/exav-viewer/demo/examples/files/archives/sample.zip create mode 100644 crates/exav-viewer/demo/examples/files/archives/server.log.bz2 create mode 100644 crates/exav-viewer/demo/examples/files/archives/server.log.gz create mode 100644 crates/exav-viewer/demo/examples/files/archives/server.log.xz create mode 100644 crates/exav-viewer/demo/examples/files/cad/blocks.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/bracket.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/dimensions.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/hatch-gallery.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/house-elevations.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/house-plan-r2000.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/house-plan.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/legacy-r12.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/linetypes.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/site-plan.dxf create mode 100644 crates/exav-viewer/demo/examples/files/cad/text-styles.dxf create mode 100644 crates/exav-viewer/demo/examples/files/converted/deck.fodp create mode 100644 crates/exav-viewer/demo/examples/files/converted/deck.odp create mode 100644 crates/exav-viewer/demo/examples/files/converted/deck.ppt create mode 100644 crates/exav-viewer/demo/examples/files/converted/diagram.fodg create mode 100644 crates/exav-viewer/demo/examples/files/converted/diagram.odg create mode 100644 crates/exav-viewer/demo/examples/files/converted/handbook.doc create mode 100644 crates/exav-viewer/demo/examples/files/converted/handbook.fodt create mode 100644 crates/exav-viewer/demo/examples/files/converted/handbook.odt create mode 100644 crates/exav-viewer/demo/examples/files/converted/handbook.rtf create mode 100644 crates/exav-viewer/demo/examples/files/converted/workbook.fods create mode 100644 crates/exav-viewer/demo/examples/files/converted/workbook.ods create mode 100644 crates/exav-viewer/demo/examples/files/converted/workbook.xls create mode 100644 crates/exav-viewer/demo/examples/files/ifc/house.ifc create mode 100644 crates/exav-viewer/demo/examples/files/images/bounce.gif create mode 100644 crates/exav-viewer/demo/examples/files/images/bounce.jxl create mode 100644 crates/exav-viewer/demo/examples/files/images/bounce.png create mode 100644 crates/exav-viewer/demo/examples/files/images/bounce.webp create mode 100644 crates/exav-viewer/demo/examples/files/images/fax-g3-1d.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/fax-g4-3pages.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/fax-g4.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/gradient-grey16.png create mode 100644 crates/exav-viewer/demo/examples/files/images/gradient-grey16.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/icon-legacy.ico create mode 100644 crates/exav-viewer/demo/examples/files/images/icon.ico create mode 100644 crates/exav-viewer/demo/examples/files/images/page-generic.jb2 create mode 100644 crates/exav-viewer/demo/examples/files/images/page-symbol.jb2 create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-1bit.bmp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-24.bmp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-32.bmp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-8.bmp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.jp2 create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.jxl create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.pam create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.png create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.qoi create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-alpha.webp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-ascii.ppm create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-bc1.dds create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-bc3.dds create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-cmyk-packbits.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-cmyk.jpg create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-grey.jp2 create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-grey.jpg create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-grey16.pgm create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-indexed.png create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-jpeg.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-lossless.jxl create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-lossless.webp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-lossy.jxl create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-lzw.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-orientation6.jpg create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-pages.tif create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-progressive.jpg create mode 100644 crates/exav-viewer/demo/examples/files/images/scene-rle8.bmp create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.ff create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.gif create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.j2k create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.jp2 create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.jpg create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.pbm create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.png create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.ppm create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.qoi create mode 100644 crates/exav-viewer/demo/examples/files/images/scene.webp create mode 100644 crates/exav-viewer/demo/examples/files/images/sunset-flat.hdr create mode 100644 crates/exav-viewer/demo/examples/files/images/sunset.hdr create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.flac create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.m4a create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.mp3 create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.ogg create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.opus create mode 100644 crates/exav-viewer/demo/examples/files/media/canon.wav create mode 100644 crates/exav-viewer/demo/examples/files/media/testcard.mp4 create mode 100644 crates/exav-viewer/demo/examples/files/media/testcard.webm create mode 100644 crates/exav-viewer/demo/examples/files/models/arm.glb create mode 100644 crates/exav-viewer/demo/examples/files/models/bracket-ascii.stl create mode 100644 crates/exav-viewer/demo/examples/files/models/gear.stl create mode 100644 crates/exav-viewer/demo/examples/files/models/icosphere-ascii.ply create mode 100644 crates/exav-viewer/demo/examples/files/models/knot.obj create mode 100644 crates/exav-viewer/demo/examples/files/models/table.gltf create mode 100644 crates/exav-viewer/demo/examples/files/models/terrain.ply create mode 100644 crates/exav-viewer/demo/examples/files/office/deck.pptx create mode 100644 crates/exav-viewer/demo/examples/files/office/handbook.docx create mode 100644 crates/exav-viewer/demo/examples/files/office/workbook.xlsx create mode 100644 crates/exav-viewer/demo/examples/files/pdf/annotations.pdf create mode 100644 crates/exav-viewer/demo/examples/files/pdf/application-form.pdf create mode 100644 crates/exav-viewer/demo/examples/files/pdf/field-report.pdf create mode 100644 crates/exav-viewer/demo/examples/files/pdf/handbook-tagged.pdf create mode 100644 crates/exav-viewer/demo/examples/files/pdf/scan-mixed.pdf create mode 100644 crates/exav-viewer/demo/examples/files/pdf/vector-art.pdf create mode 100644 crates/exav-viewer/demo/examples/files/text/bits.c create mode 100644 crates/exav-viewer/demo/examples/files/text/changes.diff create mode 100644 crates/exav-viewer/demo/examples/files/text/config.yaml create mode 100644 crates/exav-viewer/demo/examples/files/text/deploy.sh create mode 100644 crates/exav-viewer/demo/examples/files/text/feed.xml create mode 100644 crates/exav-viewer/demo/examples/files/text/field-guide.md create mode 100644 crates/exav-viewer/demo/examples/files/text/field-notes.txt create mode 100644 crates/exav-viewer/demo/examples/files/text/harbour-log.csv create mode 100644 crates/exav-viewer/demo/examples/files/text/life.py create mode 100644 crates/exav-viewer/demo/examples/files/text/lru.rs create mode 100644 crates/exav-viewer/demo/examples/files/text/page.html create mode 100644 crates/exav-viewer/demo/examples/files/text/places.kml create mode 100644 crates/exav-viewer/demo/examples/files/text/prix.csv create mode 100644 crates/exav-viewer/demo/examples/files/text/pyproject.toml create mode 100644 crates/exav-viewer/demo/examples/files/text/recipes.json create mode 100644 crates/exav-viewer/demo/examples/files/text/report.sql create mode 100644 crates/exav-viewer/demo/examples/files/text/route.gpx create mode 100644 crates/exav-viewer/demo/examples/files/text/server.log create mode 100644 crates/exav-viewer/demo/examples/files/text/style.css create mode 100644 crates/exav-viewer/demo/examples/files/text/subtitles.srt create mode 100644 crates/exav-viewer/demo/examples/files/text/tides.tsv create mode 100644 crates/exav-viewer/demo/examples/files/text/tokens.ts create mode 100644 crates/exav-viewer/demo/examples/generate/catalog.json create mode 100644 crates/exav-viewer/demo/examples/generate/make-archives.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-attributions.mjs create mode 100644 crates/exav-viewer/demo/examples/generate/make-cad.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-ifc.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-images.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-manifest.mjs create mode 100644 crates/exav-viewer/demo/examples/generate/make-media.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-models.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-office.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-pdf.py create mode 100644 crates/exav-viewer/demo/examples/generate/make-text.py create mode 100644 crates/exav-viewer/demo/examples/generate/verify-evidence.mjs create mode 100644 crates/exav-viewer/demo/examples/verify-open.mjs create mode 100644 crates/exav-viewer/demo/headless.html create mode 100644 crates/exav-viewer/demo/src/ExportImage.tsx create mode 100644 crates/exav-viewer/demo/src/headless.css create mode 100644 crates/exav-viewer/demo/src/headless.ts create mode 100644 crates/exav-viewer/e2e/cad-raster.spec.ts create mode 100644 crates/exav-viewer/e2e/cad.spec.ts create mode 100644 crates/exav-viewer/e2e/converted.spec.ts create mode 100644 crates/exav-viewer/e2e/docs.spec.ts create mode 100644 crates/exav-viewer/e2e/external.spec.ts create mode 100644 crates/exav-viewer/e2e/fixtures/fake-converter.js create mode 100644 crates/exav-viewer/e2e/frame-protocol.spec.ts create mode 100644 crates/exav-viewer/e2e/headless.spec.ts create mode 100644 crates/exav-viewer/e2e/meshes.spec.ts create mode 100644 crates/exav-viewer/e2e/model-raster.spec.ts create mode 100644 crates/exav-viewer/e2e/office-raster.spec.ts create mode 100644 crates/exav-viewer/e2e/progressive.spec.ts create mode 100644 crates/exav-viewer/e2e/raster-frame.spec.ts create mode 100644 crates/exav-viewer/e2e/raw.spec.ts create mode 100644 crates/exav-viewer/e2e/sheets.spec.ts create mode 100644 crates/exav-viewer/e2e/slides.spec.ts create mode 100644 crates/exav-viewer/e2e/tools.spec.ts create mode 100644 crates/exav-viewer/scripts/sync-examples.mjs create mode 100644 crates/exav-viewer/src/docs.rs create mode 100644 crates/exav-viewer/src/sheets.rs create mode 100644 crates/exav-viewer/src/slides.rs create mode 100644 crates/exav-viewer/ts/cad/export.ts create mode 100644 crates/exav-viewer/ts/cad/raster.test.ts create mode 100644 crates/exav-viewer/ts/cad/raster.ts create mode 100644 crates/exav-viewer/ts/cad/renderer/sdf.test.ts create mode 100644 crates/exav-viewer/ts/cad/renderer/sdf.ts create mode 100644 crates/exav-viewer/ts/cad/warnings.test.ts create mode 100644 crates/exav-viewer/ts/cad/warnings.ts create mode 100644 crates/exav-viewer/ts/cad/zoom-gesture.test.ts create mode 100644 crates/exav-viewer/ts/cad/zoom-gesture.ts create mode 100644 crates/exav-viewer/ts/core/messages.test.ts create mode 100644 crates/exav-viewer/ts/core/messages.ts create mode 100644 crates/exav-viewer/ts/core/pictures.test.ts create mode 100644 crates/exav-viewer/ts/core/pictures.ts create mode 100644 crates/exav-viewer/ts/core/print.ts create mode 100644 crates/exav-viewer/ts/core/raster.test.ts create mode 100644 crates/exav-viewer/ts/core/raster.ts create mode 100644 crates/exav-viewer/ts/core/registry.test.ts create mode 100644 crates/exav-viewer/ts/core/theme.ts create mode 100644 crates/exav-viewer/ts/image/svg.test.ts create mode 100644 crates/exav-viewer/ts/image/svg.ts create mode 100644 crates/exav-viewer/ts/media/media.test.ts create mode 100644 crates/exav-viewer/ts/model/camera.test.ts create mode 100644 crates/exav-viewer/ts/model/camera.ts create mode 100644 crates/exav-viewer/ts/model/raster.test.ts create mode 100644 crates/exav-viewer/ts/model/raster.ts create mode 100644 crates/exav-viewer/ts/model/storeys.test.ts create mode 100644 crates/exav-viewer/ts/model/storeys.ts create mode 100644 crates/exav-viewer/ts/office/convert.test.ts create mode 100644 crates/exav-viewer/ts/office/convert.ts create mode 100644 crates/exav-viewer/ts/office/docs-convert.test.ts create mode 100644 crates/exav-viewer/ts/office/docs.test.ts create mode 100644 crates/exav-viewer/ts/office/docs.ts create mode 100644 crates/exav-viewer/ts/office/docs.worker.ts create mode 100644 crates/exav-viewer/ts/office/export.test.ts create mode 100644 crates/exav-viewer/ts/office/export.ts create mode 100644 crates/exav-viewer/ts/office/external.ts create mode 100644 crates/exav-viewer/ts/office/progressive.test.ts create mode 100644 crates/exav-viewer/ts/office/progressive.ts create mode 100644 crates/exav-viewer/ts/office/raster.test.ts create mode 100644 crates/exav-viewer/ts/office/raster.ts create mode 100644 crates/exav-viewer/ts/office/renderer-raster.test.ts create mode 100644 crates/exav-viewer/ts/office/renderer.test.ts create mode 100644 crates/exav-viewer/ts/office/route.ts create mode 100644 crates/exav-viewer/ts/office/sheets.test.ts create mode 100644 crates/exav-viewer/ts/office/sheets.ts create mode 100644 crates/exav-viewer/ts/office/sheets.worker.ts create mode 100644 crates/exav-viewer/ts/office/slides.test.ts create mode 100644 crates/exav-viewer/ts/office/slides.ts create mode 100644 crates/exav-viewer/ts/office/slides.worker.ts create mode 100644 crates/exav-viewer/ts/office/table.test.ts create mode 100644 crates/exav-viewer/ts/office/table.ts create mode 100644 crates/exav-viewer/ts/office/viewport.test.ts create mode 100644 crates/exav-viewer/ts/office/viewport.ts create mode 100644 crates/exav-viewer/ts/test/fake-canvas.ts create mode 100644 crates/exav-viewer/ts/test/markdown.ts create mode 100644 crates/exav-viewer/ts/test/zip-members.ts create mode 100644 crates/exav-viewer/ts/text/decode.test.ts create mode 100644 crates/exav-viewer/ts/text/decode.ts create mode 100644 crates/exav-viewer/ts/text/index.ts create mode 100644 crates/exav-viewer/ts/text/json.test.ts create mode 100644 crates/exav-viewer/ts/text/json.ts create mode 100644 crates/exav-viewer/ts/text/lines.ts create mode 100644 crates/exav-viewer/ts/text/markdown-engine.ts create mode 100644 crates/exav-viewer/ts/text/markdown-view.ts create mode 100644 crates/exav-viewer/ts/text/markdown.test.ts create mode 100644 crates/exav-viewer/ts/text/markdown.ts create mode 100644 crates/exav-viewer/ts/text/markdown.worker.ts create mode 100644 crates/exav-viewer/ts/text/renderer.test.ts create mode 100644 crates/exav-viewer/ts/text/renderer.ts create mode 100644 crates/exav-viewer/ts/text/xml.test.ts create mode 100644 crates/exav-viewer/ts/text/xml.ts create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/.cargo_vcs_info.json create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/Cargo.lock create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/Cargo.toml create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/EXAV.md create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/LICENSE create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/README.md create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/color.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/convenience.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/data_types.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/decoder.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/box_parser.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/codestream_parser/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/codestream_parser/non_section.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/codestream_parser/sections.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/inner/process.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/input.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/moxcms_wrapper.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/options.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/signature.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/api/xyb_constants.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/bit_reader.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/color/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/color/tf.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/container/box_header.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/container/frame_index.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/container/gain_map.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/container/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/container/parse.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/ans.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/context_map.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/decode.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/huffman.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/hybrid_uint.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/entropy_coding/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/error.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/blending.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/epf.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/noise.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/patches.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/features/spline.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/adaptive_lf_smoothing.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/block_context_map.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/coeff_order.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/color_correlation_map.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/decode.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/group.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/lf_preview.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/borrowed_buffers.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/decode/bitstream.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/decode/channel.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/decode/common.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/decode/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/decode/specialized_trees.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/predict.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/transforms/apply.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/transforms/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/transforms/palette.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/transforms/rct.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/transforms/squeeze.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/modular/tree.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/quant_weights.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/quantizer.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/frame/render.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/bit_depth.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/color_encoding.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/encodings.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/extra_channels.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/frame_header.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/image_metadata.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/modular.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/permutation.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/size.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/toc.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/headers/transform_data.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/icc/header.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/icc/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/icc/stream.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/icc/tag.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/data_type.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/internal.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/output_buffer.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/raw.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/rect.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/test.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/image/typed.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/jpeg/data.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/jpeg/jbrd.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/jpeg/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/jpeg/writer.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/lib.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/buffer_splitter.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/builder.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/channels.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/internal.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/group_scheduler.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/helpers.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/render_group.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/row_buffers.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/run_stage.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/save/identity.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/low_memory_pipeline/save/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/save.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/simd_utils.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/simple_pipeline/extend.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/simple_pipeline/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/simple_pipeline/run_stage.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/simple_pipeline/save.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/black.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/blending.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/chroma_upsample.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/cms.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/cms_cmyk.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/convert.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/common.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/epf0.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/epf1.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/epf2.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/epf/test.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/extend.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/from_linear.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/gaborish.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/nearest_neighbor.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/noise.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/patches.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/premultiply_alpha.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/splines.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/spot.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/to_linear.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/tone_mapping.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/unpremultiply_alpha.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/upsample.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/xyb.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/stages/ycbcr.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/render/test.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct16.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct2.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct2d.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct32.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct4.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct8.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/idct_large.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct16.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct2.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct2d.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct32.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct4.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/reinterpreting_dct8.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/tests.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/transform.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/transforms/transform_map.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/bits.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/cacheline.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/clock.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/concat_slice.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/fast_math.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/float16.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/linalg.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/log2.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/memory_tracker.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/mirror.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/mod.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/ndarray.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/profiling.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/rational_poly.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/shift_right_ceil.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/test.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/tracing_wrappers.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/vec_helpers.rs create mode 100644 crates/exav-viewer/vendor/zenjxl-decoder/src/util/xorshift128plus.rs create mode 100644 crates/exav/tests/fixtures/split/e.z01 create mode 100644 crates/exav/tests/fixtures/split/e.zip create mode 100644 crates/exav/tests/office_text.rs create mode 100644 crates/exav/tests/split_volumes.rs create mode 100644 docs/DIFFTEST_TRIAGE.md create mode 100644 docs/PDF-RENDERER-EVALUATION.md create mode 100644 formats/registry.json create mode 100644 fuzz/dicts/markdown.dict create mode 100644 fuzz/fuzz_targets/docs_check.rs create mode 100644 fuzz/fuzz_targets/docs_doc.rs create mode 100644 fuzz/fuzz_targets/docs_odt.rs create mode 100644 fuzz/fuzz_targets/docs_pages.rs create mode 100644 fuzz/fuzz_targets/docs_rtf.rs create mode 100644 fuzz/fuzz_targets/docs_wml.rs create mode 100644 fuzz/fuzz_targets/markdown.rs create mode 100644 fuzz/fuzz_targets/mesh.rs create mode 100644 fuzz/fuzz_targets/raw_preview.rs create mode 100644 fuzz/fuzz_targets/sheets_check.rs create mode 100644 fuzz/fuzz_targets/sheets_numbers.rs create mode 100644 fuzz/fuzz_targets/sheets_ods.rs create mode 100644 fuzz/fuzz_targets/sheets_text.rs create mode 100644 fuzz/fuzz_targets/sheets_xls.rs create mode 100644 fuzz/fuzz_targets/slides_check.rs create mode 100644 fuzz/fuzz_targets/slides_keynote.rs create mode 100644 fuzz/fuzz_targets/slides_odg.rs create mode 100644 fuzz/fuzz_targets/slides_odp.rs create mode 100644 fuzz/fuzz_targets/slides_ppt.rs create mode 100644 fuzz/fuzz_targets/slides_vsdx.rs create mode 100644 fuzz/fuzz_targets/vdisk.rs create mode 100644 fuzz/fuzz_targets/visible_text.rs create mode 100644 fuzz/seeds/docs_doc/word6.cfb create mode 100644 fuzz/seeds/sheets_numbers/index09.xml create mode 100644 fuzz/seeds/slides_keynote/keynote1.apxl create mode 100644 fuzz/seeds/slides_keynote/keynote2.apxl create mode 100644 fuzz/seeds/slides_ppt/powerpoint95.bin create mode 100644 fuzz/seeds/vdisk/qcow2.bin create mode 100644 fuzz/seeds/vdisk/qcow2_deflate.bin create mode 100644 fuzz/seeds/vdisk/qcow2_zstd.bin create mode 100644 fuzz/seeds/vdisk/vhd.bin create mode 100644 fuzz/seeds/vdisk/vmdk_sparse.bin create mode 100644 fuzz/seeds/vdisk/vmdk_stream.bin create mode 100644 fuzz/support/src/panics.rs create mode 100644 scripts/formats.mjs create mode 100644 scripts/pdf-compare/README.md create mode 100644 scripts/pdf-compare/compare3.py create mode 100644 scripts/pdf-compare/hayro-eval/Cargo.toml create mode 100644 scripts/pdf-compare/hayro-eval/src/main.rs create mode 100644 scripts/pdf-compare/hayro-wasm.mjs create mode 100644 scripts/pdf-compare/hayro-wasm/Cargo.toml create mode 100644 scripts/pdf-compare/hayro-wasm/src/lib.rs create mode 100644 scripts/pdf-compare/pdfjs.mjs create mode 100644 scripts/pdf-compare/run_hayro.py create mode 100644 scripts/pdf-compare/run_poppler.py create mode 100644 www/public/og.png create mode 100644 www/public/robots.txt create mode 100644 www/scripts/make-og-image.py create mode 100644 www/src/content/docs/project/formats.md create mode 100644 www/src/content/docs/viewer/examples.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c919b1d..89261e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,24 @@ jobs: # default build, so lint it in a dedicated pass. - run: cargo clippy -p exav-core --all-targets --features unstable-internals -- -D warnings - run: cargo clippy -p exav-render --all-targets --features dwg -- -D warnings - - run: cargo clippy -p exav-render --all-targets --no-default-features --features ifc,stl -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features ifc,stl,obj,ply,gltf -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features markdown -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features text-pdf -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features office-sheets -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features office-slides -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features office-docs -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features office,office-test-util -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features -- -D warnings + - run: cargo clippy -p exav-render --all-targets --no-default-features --features text-office,office-test-util -- -D warnings + - run: cargo clippy -p exav-core --all-targets --features visible-text-office -- -D warnings + - run: cargo clippy -p exav-grep --all-targets --features text-office -- -D warnings + - run: cargo clippy -p exav-unpack --all-targets --features text-pdf -- -D warnings # The full native test matrix, every feature pass. Defined once in the # Makefile so this and a local `make test` can't drift. - run: make test-native - # The promised floor is `rust-version` in Cargo.toml (1.92, imposed by the - # hayro image decoders). `make msrv` reads it there and installs that toolchain on + # The promised floor is `rust-version` in Cargo.toml (1.95, imposed by the + # delharc LHA reader). `make msrv` reads it there and installs that toolchain on # demand, so a bump flows through with no edit here, and a contributor on # stable can't land a newer feature without this going red. msrv: @@ -194,7 +205,7 @@ jobs: # the masked fixtures are unmasked. seeds="$RUNNER_TEMP/fuzz-seeds" scripts/fuzz-seeds.sh "$seeds" - for t in filetype sigs cvd pe unpack analyze bytecode full_pipeline seekable yara pcre clamd icap volumes authenticode database ndb_compile rar3_ppmd pe_emulator x86_decode parser_recursion imagehash drawing cad_dxf cad_dwg ifc; do + for t in filetype sigs cvd pe unpack analyze bytecode full_pipeline seekable yara pcre clamd icap volumes authenticode database ndb_compile rar3_ppmd pe_emulator x86_decode parser_recursion imagehash drawing cad_dxf cad_dwg ifc mesh; do mkdir -p "fuzz/corpus/$t" extra="" [ -d "$seeds/$t" ] && extra="$seeds/$t" diff --git a/.github/workflows/www.yml b/.github/workflows/www.yml index bdf952f..68659e8 100644 --- a/.github/workflows/www.yml +++ b/.github/workflows/www.yml @@ -49,6 +49,10 @@ jobs: --site "${{ steps.pages.outputs.origin }}" \ --base "${{ steps.pages.outputs.base_path }}" working-directory: ${{ env.BUILD_PATH }} + # Every internal link and #anchor of the built pages must resolve (offline, + # a second or two). The demo's own pages are copied in below. + - name: Check internal links + run: node scripts/check-links.mjs ${{ env.BUILD_PATH }}/dist # The @exav/viewer demo, served at /viewer/demo/: the package and its wasm # modules built from this commit, the archive engine's pkg/ too. - uses: dtolnay/rust-toolchain@stable @@ -71,8 +75,14 @@ jobs: npm ci npm run build:wasm npm run build - # The sidebar's files: github.com/sylvinus/exav-samples, HEAD. + # The sidebar's files, from the network: github.com/sylvinus/exav-samples, HEAD. npm run demo:showcase + # `demo:build` runs `npm run demo:examples` first: the example files + # committed under demo/examples/files/ are copied, and the others (tens + # of MB, from Zenodo, figshare, Wikimedia Commons and the like) are + # downloaded at build time, each checked against its size and SHA-256 in + # demo/examples/examples.json. The build fails when one cannot be fetched + # or has changed. # DEMO_ORIGIN goes into the sandboxed frame's policy, for Safari. DEMO_BASE="${{ steps.pages.outputs.base_path }}/viewer/demo/" DEMO_ORIGIN="${{ steps.pages.outputs.origin }}" npm run demo:build # dist/viewer/ holds the File viewer docs; the demo must not land on them. diff --git a/.oss-scanner/Dockerfile b/.oss-scanner/Dockerfile index 420c585..9795919 100644 --- a/.oss-scanner/Dockerfile +++ b/.oss-scanner/Dockerfile @@ -4,7 +4,7 @@ # # Build it the way the scanner does, from the repository root: # docker build -f .oss-scanner/Dockerfile -t exav-oss-scanner . -FROM rust:1.92-bookworm +FROM rust:1.95-bookworm # Frame pointers and line tables for a debugger, without the full debug info # that makes the build several times larger. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df5588..16744a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,484 @@ follow [semantic versioning](https://semver.org/). ## [Unreleased] +### Added + +- **Pictures of pages from the formats that draw something.** + `controllers.pictures` gives `raster(page, options)`, `toBlob(page, options)` + and `render(page, longEdge)`. A page is drawn again from the document's own + data at the size asked for, with a `region` in the format's coordinates (a + fraction of a page, pixels of an image, drawing units with `layout`, a 3D + camera) or `"current"`, `fit` ("contain" or "cover"), `lineweight`, + `background` and an `AbortSignal`. The result reports the region actually + drawn. `controllers.viewport` is what the session shows now, in the same + coordinates. Errors are a `RasterError` (`unsupported`, `too-large`, + `aborted`, `invalid`, `failed`). Pictures are limited to 16,384 px a side and + 64 Mi pixels in all, in the page and in the sandboxed frame, and are encoded + by the host page. The demo has an "Export image" button beside the file's + name, which opens a dialog of options. +- **A full screen button** in the viewer's tools (`FullscreenToggle`, a piece a + host can replace) puts the viewer's box in full screen, from the host page, + and leaves it. It is not shown where the browser has no full screen. Hosts + drawing their own controls get `useFullscreen(() => element)` and + `usePanel(info?)` (the side panel's fold state, shared by the rails and the + toggle) from `@exav/viewer/react`. +- **`session.replace` works in the sandboxed frame**, so zoom and place are + kept when a host swaps a document, for PDF and images. +- **A format registry** (`formats/registry.json`, 117 formats) from which the + format matrix page and the viewer summary are generated, and which + `make test-formats` and the viewer's and the scanner's tests check against + the extractor's formats, the scanner's file types and the viewer's matchers. +- **Virtual disks read in place.** VHD (fixed and dynamic), VHDX, QCOW2 + (stored, deflate and zstd clusters) and VMDK (sparse and streamOptimized) are + read through a `Read + Seek` that reads the container's tables as the walk + reaches them and keeps only a 32 MiB cache of decoded units, so a disk of any + declared size costs what it holds. A disk over the buffer limit is walked + through its partitions and its NTFS and FAT files come out one at a time; a + smaller one is still one member. What a disk decodes is counted against the + scan budget. NTFS and FAT are read in place too, and a raw NTFS or FAT image + or partition over the limit is walked instead of refused or streamed whole. + ext is still copied into memory, as its volume only. +- **3D model pictures and viewport.** IFC, STL, OBJ, PLY and glTF expose + `controllers.pictures` (one page) and `controllers.viewport`. A picture is + redrawn from the geometry by an off-screen renderer at the requested size, in + tiles beyond GPU limits, with MSAA or supersampling and a transparent or any + CSS colour background. Regions are cameras in the file's own coordinates. + `region: "current"` is the live camera, `fit` contain or cover applies, and + hidden categories stay hidden. The `rasterTile` and `rasterSupersample` + plugin options are for testing and tuning. +- **DWG and DXF pictures and viewport.** `pictures.raster` draws any layout + again from the drawing's vectors at the asked pixel size, in a WebGL context + of its own (tiled and composed past the GPU's limits). It takes a region in + drawing units (y up, model units or a sheet's paper units), a size, `fit`, + `lineweight` "screen" or "scaled", any CSS colour or a transparent ground, + and a `layout` id, and reports the region actually drawn. The `viewport` + store gives the layout and region in view, and `region: "current"` draws it. + One picture is drawn at a time per session, abortable. The `rasterTile` + option tunes the tile size. +- **Pictures and viewport for Word, PowerPoint, Excel and CSV.** + `controllers.pictures` draws a page, slide or sheet at any size and region + (fractions of the page; a sheet's page is its used area at 100% without + headers), and `controllers.viewport` reports the page most in view and the + visible part. The converted formats (DOC, ODT, RTF, XLS, ODS, Numbers, PPT, + ODP, Keynote, ODG and the external converter's PDF or XLSX) get them too. + Pictures are drawn from a second copy of the document and queued one at a + time. A region whose whole page would pass the engines' 16.7 M pixel limit is + refused as too large, and `background: "transparent"` is refused. +- **Pictures of a video's current frame.** `controllers.pictures` on video + (page 1, space "frame"). A video the page cannot read (another origin without + CORS, or a URL given to the sandboxed frame) is refused as unsupported + instead of drawn blank. +- **IFC curved faces, trimming, alignments and storeys.** B-rep faces on + spheres, tori and cylinders (sphere caps and seamless tubes included) are + meshed in their surface's parameters instead of flat. B-spline faces are cut + by their bounds, holes included. `StartParam`/`EndParam` of swept disks and + directrix sweeps are honoured. IFC4X3 curve segments (lines, circles, + clothoid and polynomial spirals, polynomial curves), gradient curves and + linear placements without a Cartesian position are evaluated, and + `IfcGridPlacement` sits at its grid intersection. exav-render reports + `Element::storey`, `Batch::storey` and `Node::elevation`, and `@exav/viewer` + has a `storeys` controller with a show/hide list. Conical faces + (`IfcSurfaceOfRevolution`) are still drawn flat. +- **Camera RAW files open in the viewer and in exav-render**, shown by the + largest JPEG or RGB preview they embed, turned by their Exif orientation + (`exav_render::image::raw`, feature `raw`, default; `decode_any` reads a RAW + file by its preview, `Format::detect` and `decode` still read a TIFF-structured + one as the TIFF it also is, so exav-imagehash is unchanged). DNG, Canon CR2, + CR3 and CRW, Nikon NEF and NRW, Sony ARW, SR2 and SRF, Olympus ORF, Panasonic + RW2, RAW and Leica RWL, Fujifilm RAF, Pentax PEF, Samsung SRW, Hasselblad 3FR, + Phase One IIQ, Sigma X3F, Kodak, Epson and Leaf files are told by their bytes, + never their names, and a plain TIFF stays a TIFF. The sensor data is not + decoded: a file that embeds no preview (most phones' DNG, Minolta MRW, the + newest CR3 whose preview is HEIF) is `Error::NoPreview`, and the viewer says + so (`image_no_preview`). Every offset and count in the file is bounds-checked, + directory chains and loops are capped, and previews count against the decode + budget as the RGBA a page holds. New `raw_preview` fuzz target. +- **The visible text of legacy and flat office files.** `text-office` + (`exav-render`, `exav-grep`) and `visible-text-office` (`exav-core`, `exav`), + opt-in, put the text of DOC, XLS, PPT, RTF, Word 2003 XML, SpreadsheetML 2003, + SYLK, FODT, FODS, FODP, FODG, Numbers, Pages and Keynote in the text view. A + match is located by sheet or slide, a link is `label (target)` and + `--detect phishing` judges it; an encrypted, over-limit or damaged file makes + the scan PARTIAL, never clean. Compound files are read as office documents by + their extension only. +- **Demo examples.** 243 freely licensed example files (145 committed, 98 + fetched by `npm run demo:examples` and checked against their SHA-256), with a + manifest, an attributions page and a NOTICE. +- **Office documents, spreadsheets, presentations and drawings converted in + safe Rust, inside exav-render.** The features `office-sheets`, + `office-slides`, `office-docs` and `office` (`exav_render::office`; none is + a default feature) turn XLS (BIFF2 to BIFF8), ODS, FODS, OTS, Numbers ('08 and + later), Excel 2003 XML, HTML tables and SYLK into XLSX; PowerPoint 95 to 2003 + (with .pps and .pot), ODP, OTP, FODP and Keynote 1 and later into PPTX; + DOC and DOT (Word 6 to 2003), ODT, OTT, FODT, RTF, Word 2003 XML and Apple + Pages (5 and later) into DOCX; and ODG, OTG, FODG and Visio 2013+ (VSDX, VSDM, + VSTX, VSTM) drawings into PPTX, a page to a slide. @exav/viewer shows the + result with `xlsx()`, `pptx()` and `docx()`, in a worker per file, from the + `sheets`, `slides` and `docs` wasm modules (450, 434 and 359 KB brotli). The + files are read as they are + stored, nothing recalculated; tables, lists, sections, notes, fields, + pictures, charts and shapes are kept; tracked changes in DOC are drawn as + strike-through and underline. Input, cells, paragraphs, memory and + decompressed parts are bounded (`Limits`); an encrypted file, one over its + limits or a damaged one is reported with its reason. Fuzz targets + `sheets_xls`, `sheets_ods`, `sheets_numbers`, `sheets_text`, `slides_ppt`, + `slides_odp`, `slides_keynote`, `slides_odg`, `slides_vsdx`, `docs_doc`, + `docs_odt`, `docs_rtf`, `docs_wml` and `docs_pages`. +- **Older presentations.** PowerPoint 95 and Keynote 1 to 5 (XML) are read. + PowerPoint 2 to 4 (Windows, Macintosh, BinHex) are recognised and reported as + unsupported. A presentation the converter recognises and cannot convert ends + in an error with its kind. Also added: picture bullets (ODP, PPT), + charts stored in PPT files as ODF packages, Keynote tables, charts and movies, + inline pictures and charts in flat ODP, tile sizes from the picture's + resolution, text areas of custom shapes, per-path fill and stroke, even-odd + fills and the default style of unstyled shapes. +- **Large decks and long documents are laid out progressively.** The viewer + asks @silurus/ooxml for `progressiveLayout` past 32 slides or 64 KB of + inflated `document.xml`; on a 300-page document the first page shows in about + 2 s instead of about 19 s. +- **`maxArchiveEntries` for Word, Excel, PowerPoint and CSV** (`docx()`, + `xlsx()`, `pptx()`, `csv()`): the most entries a document's ZIP may list. + The default is now 20000; @silurus/ooxml's own 4096 refused a deck of about a + thousand slides with pictures. The size a document may inflate to keeps its + limit. +- **Volume sets are scanned as the archive they are.** A RAR set + (`x.part1.rar`, `x.r00`), a ZIP set (`x.z01`, ..., `x.zip`) and a CAB set (the + cabinets name one another) are rejoined and scanned whole, when their volumes + sit in one directory (`exav DIR`), when one volume is named and its siblings + are next to it (regular files only, never a symbolic link), and when they are + sent together to the daemon. A set with a volume missing is `PARTIAL`, never + clean. A cabinet read alone now reports the member that continues in another + one instead of leaving it out. +- **ELF and Mach-O `EP` and section offsets** (`EP`, `Sn`, `SL`, `SEn`) match + as they do in clamscan, read from the headers. Each was measured against + clamscan 1.4.3. `EP` on a Mach-O is not resolved there either. +- **The viewer opens more archives:** cab, chm, iso, lzh, arj, ar and deb, + cpio, xar and pkg, dmg, wim, zst, lz4, lz and Z files, tarballs of each + compression, zoo, arc, alz, egg, jar and the other packages that are zips + (apk, ipa, xpi, whl, nupkg, vsix, msix), comic books (cbz, cbr, cb7, cbt), + virtual disks (vhd, vhdx, vmdk, qcow2) and `winmail.dat`, by name, by the + type a desktop gives the file, or by the first bytes for a member of an + archive. A `.tgz` typed `application/x-compressed-tar`, as a Linux desktop + types it, is claimed now. +- **Text previews in the viewer** (`text()`, `@exav/viewer/text`): plain text, + logs, Markdown, JSON, XML, YAML, TOML and INI, source code, subtitles and + diffs, and HTML as source. Nothing in the file is parsed as markup; Markdown + is read in WebAssembly by pulldown-cmark (CommonMark, tables, strikethrough, + entities and reference links) through exav-render's new `markdown` feature + (`forbid(unsafe_code)`, linear on every hostile input tried, a `markdown` + fuzz target), and drawn element by element. A Markdown link shows its address + and opens it on a click, as a PDF's links do (the host asks first in the + frame), images are never loaded, bidirectional and control characters are made + visible, a file is cut at 2 MiB and 50,000 lines with a warning, and a binary + file is refused. The new `exav_viewer_text.wasm` module is about 190 KB (82 KB + gzipped), loaded when a Markdown file is opened; if it fails to load, the + text is shown as it is with a warning. +- **A visible-text layer, in the scanner and in `exav-grep`.** A document is + matched through the text a reader sees as well as its bytes: its words without + the marks, tags, escapes or run boundaries, entities decoded, and each link + followed by where it goes (`label (target)`, even for a `javascript:` target + or one with credentials), as a text view that `Target:0` and `Target:7` + signatures see. That lets a signature match a phrase Word split across runs, + a PDF kerned into pieces, or a link whose label names one site over a target + on another. Read: Markdown (pulldown-cmark), HTML (html5gum's tokenizer, no + tree), the XML parts of Word, PowerPoint, Excel and OpenDocument files by + their path (quick-xml), AutoCAD drawings (the text, attributes, dimension and + multileader texts, block and layer names, from exav-render's drawing model), as + the input or as an archive member; and PDF (hayro) with the `visible-text-pdf` + feature, which is not a default (see below). With `--alert-phishing`, a link + whose label names one site and whose target another is now judged in these + documents as an HTML anchor is, which also catches an HTML link whose target is + written in entities. It is the new `visible-text` feature of `exav-core` and + `exav` (default; off under `--clamav-compat`); an object past + `--max-object-bytes` is not read and the scan is reported incomplete. + `exav-grep --text` searches the same text, reporting `file.md!text` with the + text's line numbers. exav-unpack has the `text` and `text-pdf` features and + `exav_unpack::text`, exav-render `text` and `text-pdf` and + `exav_render::text::visible_text`, and there is a `visible_text` fuzz target. + RTF and email headers have none yet (see `docs/BACKLOG.md`). +- **PDF text is opt-in (`visible-text-pdf`, `exav-grep`'s `text-pdf`).** + hayro-interpret turns on `hayro-syntax`'s `unsafe` feature itself, which + switches every `flate2` in the build to zlib-rs (a damaged stream then gives + back other bytes: two of exav-unpack's tests fail with it) and adds SIMD to + the image decoders. hayro also puts no bound on what a stream decompresses to + (a 1 MB PDF took 2.1 GB), so exav gives it a PDF only after its own reader has + taken every stream within the limits. Built without hayro's predefined CJK + CMaps, which would bring `brotli`. +- **More 3D models in the viewer and exav-render:** Wavefront OBJ, Stanford + PLY (ASCII and binary) and glTF 2.0 (`.gltf` with embedded buffers, `.glb`), + read in safe Rust with bounded triangulation, JSON, node trees and + accessors. Nothing a model names is fetched; a glTF that needs an external + file is reported, not shown. There is a `mesh` fuzz target for them. +- **JPEG XL in the viewer** (`.jxl`, first frame, 8-bit), decoded in + WebAssembly by zenjxl-decoder, a fork of the libjxl team's jxl-rs that is + `forbid(unsafe_code)`, within the viewer's decode budget. The image module + grows from 1.1 to 1.65 MB. exav-render has it as the `jxl` feature, off by + default. +- **A host can draw the whole viewer UI itself, the files still sandboxed.** + The frame draws the document and nothing else; new controllers give the + rest to the page: `media` (play, pause, seek, volume, rate; `video({ + controls: "none" })` drops the browser's own player controls), `sheets` (an + Excel workbook's sheets, whose tabs the UI now draws: the engine's are gone), + `pictures` (a page of a PDF or an image drawn whole as an `ImageBitmap`, for + thumbnails and printing, at most 4096 px a side), and zoom for Word, Excel + and CSV, page jumps for Word and slides. `printPictures` prints from those + pictures, which the frame cannot and which a TIFF or a PDF cannot from its + URL. The arrows and Escape pressed in the frame, when the document does not + use them, reach the page's listeners (navigation keys only, at most 30 a + second; `keys`). The frame takes the page's `--exav-viewer-*` colours and colour + scheme, read where it is mounted (`theme`, `restyle()`), so a text file or a + Markdown page in it follows a dark theme. In the frame, the iframe fills its + element without the stylesheet. `demo/headless.html` is such a host, in + plain DOM, and has the browser tests of all this. +- **SVG in the viewer**: drawn as an ``, which runs none of its script and fetches nothing it names, + and drawn again sharp when zoomed. **FLAC** is played with the rest. +- **The viewer speaks German, Spanish and Dutch**, and its strings are exported + by `@exav/viewer` for hosts without React. +- **The viewer says what a load is doing, and how far it is.** A loading + status carries a `step` (the file's bytes, the engine's download, its start, + reading, converting, drawing) and the share of that step done, where it is + counted; the default UI words each step and draws a bar, and the sandboxed + frame passes both to the host. +- **DWG and DXF drawings closer to AutoCAD's.** A polyline with width is a + filled band (its outline under `$FILLMODE` 0). Lineweights are millimetres on + a layout's sheet, shown when the drawing's `$LWDISPLAY` says so (the cad + `lineweights` option: `"drawing"`, `"always"`, `"never"`). A text crossing a + viewport's edge is cut at it rather than kept or dropped whole. A block + clipped with XCLIP shows only what is inside its boundary, with the frame + `$XCLIPFRAME` asks for. Transparency holds for an entity in its layer's or + block's colour; translucent geometry blends over everything behind it in + draw order, text included, and is no darker where a polyline's segments + meet. What is not drawn is said: entities by type (a warning's new + `names`), xrefs counted with the other external references and their path + drawn where they are inserted, the files they name listed; a drawing with + nothing drawn here (a 3D model) keeps the thumbnail it was saved with. + Text follows its block mirrored or stretched, and TEXT's backward and + upside-down flags; Bottom and Middle alignment sit on the descender as in + AutoCAD; Aligned keeps its width factor. MTEXT takes its style's width + factor and slant, and draws `\W`, `\Q`, `\T`, paragraph justification, + stacked fractions, background masks and frames; multiline attributes and a + multileader's text are drawn as MTEXT. `%%nnn` and `\X` are read; symbol.ttf, + the SHX Greek fonts and gdt.shx show the characters they stand for; text the + stroke font lacks (Chinese in an SHX style) is drawn in an outline face, + completed from the browser's fonts; an invisible constant attribute is not + drawn; Greek and Cyrillic wrap at their own widths. A drawing with more + characters than the glyph atlas affords at a close zoom draws them as + distance fields, sharp at any zoom. A hatch pattern a unit or less apart + fades as it crowds below a pixel instead of turning solid; one too dense to + draw at all fills its region in a half tone instead of leaving it empty. + 3DFACE hides the edges its flags hide; points take `$PDMODE` and + `$PDSIZE`; polyface and polygon meshes and MESH are drawn as their edges; + TOLERANCE draws its feature control frame; a leader's arrowhead is its + style's (`DIMLDRBLK`), and a multileader draws its leader to the landing + with its arrowhead. Circles, arcs and a polyline's bulges are true arcs + however close the view, no longer polygons. A dashed polyline restarts its + pattern at each vertex unless `PLINEGEN` runs it on. AutoCAD's complex + linetypes (TRACKS, ZIGZAG, FENCELINE1 and 2, BATTING) draw their shapes, + and a linetype's text sits in its gap rather than a gap's length early, + over the dash before it. A dimension saved without the block AutoCAD draws + it from, as some generators write them, is drawn from its definition + points and its style: linear, aligned, radius, diameter, angular and + ordinate, its measurement in the style's decimal places, separator and + zero suppression. Yellow, cyan and the + lightest colours are darkened on a light ground, the darkest blues lightened + on a dark one, so a line or a text stays legible (fills keep their colours); + the cad `monochrome` option draws everything in ink, as monochrome.ctb + plots. A layout is drawn on its sheet of paper, with the ground around it. + A line's anti-aliased edge blends over what lies + behind it, without the halo of paper it used to leave where it crossed + another line, and a solid fill's edge is anti-aliased too. A stray entity far from the drawing (a line left at the + origin of a plan in national grid coordinates) no longer shrinks it to a + speck nor costs it its precision; the zoom stops where precision would run + out, and the zoom controller says how far it goes. Tessellating takes a + third less memory at its peak, the page no longer keeps a copy of the + geometry once it is on the GPU, and a lost GL context comes back with the + drawing on it. + +### Changed + +- **Dependencies are at their latest versions**, except the image decoders + pinned to the versions libclamav 1.5.4 links (`image`, `png`, `gif`, + `zune-jpeg`, `fax`, `weezl`), which `exav-imagehash` needs for ClamAV's hash. + Notable: `quick-xml` 0.42, `hfsplus` and `apfs` 0.4, `cfb` 0.15, + `lzma-rust2` 0.23, `sevenz-rust2` 0.24, `delharc` 0.8 (which also reads + PMarc LHA archives), `base64` 0.23, `bit-set` 0.11, `yara-x-parser` 1.19; + and for the viewer and docs, Vite 8, vitest 5, jsdom 30 and Playwright 1.64. + This clears three RustSec advisories (`ctutils`, `rustls-webpki`, + `webpki-roots`). +- **The minimum Rust version is 1.95** (it was 1.92), which `delharc` 0.8 + requires. It also leaves a single `lzma-rust2` in the tree. +- **QCOW2** images with an external data file, extended L2 entries or other + unknown incompatible features are reported instead of misread, and **VMDK** + grain table entries are read with the header's `numGTEsPerGT`. +- **Salvage keeps more of a damaged cabinet and archive.** A CAB MSZIP block cut + or damaged part way keeps what it decoded; a cut at the end of the file is + not reported, damage is. A ZIP member of a codec the `zip` crate lacks that + fails part way keeps the bytes before the failure and is reported. An + InstallShield Z member that stops short or fails part way keeps its decoded + start and is reported, where it used to come out as whole or be dropped. +- The sandboxed frame's protocol is version 5 (it was 4): `draw` and `drawn` + became `raster` and `rastered`, `replace` and `replaced` are new, and each + `read` names its document. `MAX_PICTURE_EDGE` is 16,384 (was 4,096) with a + new `MAX_PICTURE_PIXELS`, and the `pictures` store holds `{ count, space }`. + The page and the frame must come from the same release. +- **`controllers.viewport` is worked out only while it is used.** A session that + never reads it no longer measures the layout of Word, PowerPoint and PDF + pages, and in the sandboxed frame no longer opens a second copy of every + spreadsheet's workbook, which the frame's own reading of it did. The viewport is kept up to date while the store has a + listener, and a `get()` without one works it out once (a sheet's is measured + asynchronously, so its `get()` has the last measure and starts the next). In + the sandboxed frame, the host sends `watch` when the first listener arrives + and `unwatch` when the last leaves; the frame reports the viewport only + between them, and a `get()` without a listener returns the last value the + frame reported and asks for a fresh one. The demo's "Export image" follows it + only while "What I see" is chosen. +- The viewer's description names OBJ, PLY and glTF wherever it named IFC and + STL. +- A batch of an IFC scene now holds one storey's elements, so a scene can hold + more batches. A face whose edge loop cannot be read counts in + `warnings.invalid`, and surface evaluation and edge sampling have their own + per-file budget. +- **External converter for the formats the package does not read.** A binary + Visio, CorelDRAW, Publisher, WordPerfect or other Document Liberation file + goes to an external converter: an ES module exporting + `convert(bytes, { hint, maxMemory, signal, onProgress })` that resolves to + `{ format: "pdf" | "xlsx", bytes }`, set with the `externalConverter` option, + `exavViewer({ externalConverter })` in `@exav/viewer/vite` or + `exav-viewer-assets --external-converter`; the `converter` option is `"exav"` + or `"external"`. Without one, such a file ends in the new error + `needs_converter`. +- `@silurus/ooxml` 0.89.1. +- The PPTX writer names slides (``) and writes another Application + string in `app.xml`. + +- **The sandboxed frame may autoplay** (`allow="autoplay"`, `autoplay=*` in its + `Permissions-Policy`): a player started by the page's controls would + otherwise refuse to play. Update a frame served with its own headers. The + frame's protocol is version 4: the frame and the page must come from the + same release. +- Escape handed back by the frame closes `ViewerDialog`. +- **The viewer's CSS prefix is `exav-viewer-`**, in place of `exv-`: its custom + properties (`--exav-viewer-accent`...) and its class names. Rename them in a + theme or a stylesheet written for 0.0.2. + +- A JBIG2 stream in a PDF has the same bound on the pixels of its regions as a + JBIG2 file (512 MiB), whatever memory the caller allows. +- The `extreme` sweep reports a decoder that does not end (10 s) as well as one + that panics. +- **Fewer dependencies, less `unsafe`.** DMG's LZFSE and LZVN runs are decoded + by a port of Apple's reference decoder in exav-unpack, checked against it on + damaged streams too (differential tests, run on request), in place of + `lzfse_rust`. InstallShield Z archives are + read in-tree, in place of `unshield` and `explode` (and `arraydeque`). The + vendored TIFF decoder holds 16-bit float samples as their bits, which drops + `half` and `zerocopy`; zstd is built without its checksum (`twox-hash`), which + exav never checked; and `goblin` without the parsers exav does not use. A + default build has 8 crates fewer, and 1,155 fewer `unsafe` uses in the code it + links (plus 807 in `zerocopy`'s derive macro). + +### Fixed + +- **A malformed `raster` message to the sandboxed frame is answered.** The frame + dropped one whose options failed the checks, so the host's promise stayed + pending until aborted; the frame now answers `rastered` with the error + `invalid`, which rejects with a `RasterError`. +- **QCOW2:** an L2 entry with flags and no offset read the file's own header as + disk data. **VMDK:** a grain table entry of 1, a zeroed grain, read sector 1 + of the file. +- **LZFSE, LZVN and DCL on damaged streams.** An LZVN payload whose end opcode + was read but whose declared size exceeded the input was reported as cut + instead of damaged. An LZVN instruction past the block's declared size was + called damage before the output limit was applied, dropping the part that + fits. A DCL stream with a bad first header byte and no second was reported + cut. Found by comparing the decoders with Apple's lzfse and zlib's blast on + damaged streams. +- IFC: a cylindrical B-rep face bounded by two circles was drawn flat, a + B-spline face was drawn over its whole surface, and a grid-placed element sat + at its parent's origin. +- **PDF strings:** `\b` and `\f` were read as the letters, which gave a wrong + owner key, so an encrypted PDF that opens with an empty password was reported + password-protected. A short octal escape such as `\0` also swallowed the byte + after it, closing parenthesis included. Found by a differential run against + ClamAV (`docs/DIFFTEST_TRIAGE.md`). +- **An object is scanned once per scan when its bytes were already scanned + clean** in the same place: same container type, same types above it, same + name extension, at the same depth or shallower. A document that holds copies + of itself used to be scanned once per copy (a 5 MB file that unpacks to 660 + MB, 152 members of which 50 differ, took 30 s and now takes 3 s). Only a + member of 64 KiB to 512 MiB that came out clean and complete is remembered, + by a SHA-256 of its bytes and place; a copy that is not clean, or that sits + deeper, is scanned as before. +- **Carving** counts a ZIP stapled into an object once. Each local header after + the first used to be an archive candidate of its own: a zip of a few dozen + entries filled the cap of 32 and the object was reported over a limit, and the + rest of the object was rescanned once per entry. A header at the offset the + previous one says the next entry begins (no data descriptor, encryption or + ZIP64 size) is no longer a candidate; the first one's walk reads it. + +- **A zstd stream of a few kilobytes can no longer take gigabytes of memory** + (RUSTSEC-2026-0344): ruzstd 0.9.1 bounds what one block decodes to. Also + rustls 0.23.46 (RUSTSEC-2026-0346 to 0349), in the `http` builds. +- **Two InstallShield Z members with the same path are both scanned.** The + reader kept only one of them. +- **Each dynamic block in an AutoCAD 2007 or later DWG is drawn as itself.** + AutoCAD's files can name several anonymous blocks plain `*U`, and every + insert of one drew the first; they are numbered as AutoCAD numbers them. + +Found by an 8-hour fuzzing run: + +- **An InstallShield Z archive can no longer ask for billions of members.** A + few hundred bytes of directory records made the reader build an entry for each + declared member before anything was checked (1.5 GB). A table declaring more + than 20,000 is reported as unexamined. +- **A YARA rule with an error deep inside parentheses is refused at once.** The + parser took about 2^d steps for an error inside d open parentheses (5.6 s for + 3.5 KB of rule text). Nesting past 12 is a compile error. +- **RAR key derivation is bounded for the whole scan.** The bound was per + archive, so archives inside archives each paid it, and one header asking for + 2^24 PBKDF2 iterations cost 2.6 s. It is now 2^22 iterations (about 0.7 s) for + RAR5 and 256 derivations for RAR3, counted across the scan. An archive asking + for a larger count is reported as encrypted, like one no password opens. + +Found by fuzzing the virtual disk walker and the Office readers: + +- **A FAT directory or file at cluster 1 no longer ends the walk of the whole + volume.** The `fatfs` crate subtracts 2 from a cluster number it takes from + the volume without looking at it, so a directory entry or a chain naming + cluster 1 made it panic, and the panic was caught at the walk's boundary as + an undecodable volume, losing every other file. It now ends only that + directory or file, which is reported. +- **A PowerPoint 95 bullet size of 4,294,968 percent or more no longer panics.** + The size is dropped when it is not from 25 to 400, but the product was taken + first, and overflowed: a panic in a build that checks overflow. +- **Pinch and ctrl-wheel zoom on a DWG or DXF drawing no longer lags.** A frame + was drawn for every zoom event, and on a drawing that takes the GPU longer + than a frame they queued, so the view followed the fingers seconds late. + While the zoom goes on the canvas is now scaled where it is (a CSS + transform, at most once per animation frame) and the drawing is drawn sharp + when the zoom pauses. The zoom and viewport stores are published once per + frame instead of once per event, which is one message less per event to a + host of the sandboxed frame. The glyph page the closer view needs, made of + distance fields when the text is too large for the atlas budget, was made in + one piece, which held the page's thread for hundreds of milliseconds, when + the zoom stopped; it is now made in slices of 8 ms while the old page stays up. +- **A trackpad pinch on a DWG or DXF drawing zooms in Safari.** Safari reports + the pinch as gesture events, which the drawing only cancelled; they now zoom + by their scale about the fingers, as on an image. The ctrl+wheel events Safari + may send for the same pinch are ignored while the gesture goes on, so the + zoom is not applied twice. +- **A PDF zooms out past the page fitted to the width**, down to a quarter of + it (`pdf({ minZoom })`, default 0.25 instead of 1), so a whole page is in + view on any screen. The button back to the width shows once zoomed either way. +- **The side panel's toggle is among the tools**, bottom right beside the zoom + buttons (a `PanelToggle` piece), instead of in the panel's header and a + square at the top right when it is folded. It stays on a touch screen: what + is for a mouse (the zoom and drag groups, full screen) has the attribute + `data-pointer-only`, which a host's own piece can carry too. +- **The fit button of the zoom controls is shown only once the view is not the + fitted one**, and has a "back to the start" icon instead of the full screen + one. A Word document's zoom controller now reports its scale relative to the + page fitted to the width, as the others do relative to their fit. + ## [0.0.2] - 2026-10-11 The scanner, rebuilt around one streaming scan; three new projects (a browser diff --git a/Cargo.lock b/Cargo.lock index 8737f16..6c41e58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android_system_properties" version = "0.1.6" @@ -89,19 +98,48 @@ dependencies = [ [[package]] name = "apfs" -version = "0.2.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1b4ecd96379464ea8b4ef2517c8eb674d8658ad364e9176bf5b8c278aa801e" +checksum = "6f959493f524db6102b1bd1884b58e3d632af70009441645287a7ee072944494" dependencies = [ "byteorder", + "cmpfs", "thiserror", ] [[package]] -name = "arraydeque" -version = "0.4.5" +name = "archmage" +version = "0.9.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ffd3d69bd89910509a5d31d1f1353f38ccffdd116dd0099bbd6627f7bd8ad8" +checksum = "15785ee3576fbd41276a25f063b1e6b5bf323af9b4924632e25a8de6a3065f06" +dependencies = [ + "archmage-macros", + "safe_unaligned_simd", + "winarm-cpufeatures", +] + +[[package]] +name = "archmage-macros" +version = "0.9.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce7707714970d5cf476caccbae2d282000c6c14ca5aada878e722a10d8e1c153" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "ascii_tree" @@ -110,16 +148,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643" [[package]] -name = "autocfg" -version = "1.5.1" +name = "atomic_refcell" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" [[package]] -name = "base64" -version = "0.22.1" +name = "autocfg" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -127,26 +165,24 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - [[package]] name = "bit-set" -version = "0.8.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] [[package]] name = "bitflags" @@ -187,6 +223,30 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "bstr" version = "1.13.1" @@ -209,6 +269,20 @@ name = "bytemuck" version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] [[package]] name = "byteorder" @@ -260,9 +334,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.5.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "shlex", @@ -270,9 +344,9 @@ dependencies = [ [[package]] name = "cfb" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +checksum = "0f13298d9178622b7916ec5d01d470425c04273c8ed07e87b1c38b34c4f4612b" dependencies = [ "fnv", "uuid", @@ -343,7 +417,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -358,6 +432,18 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cmpfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9532b7b405749f94737f0c75eff95f1cdd3ab030754f06998f0206aab060a0d5" +dependencies = [ + "flate2", + "lzfse_rust", + "lzvn-core", + "thiserror", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -378,9 +464,12 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9" +dependencies = [ + "rustversion", +] [[package]] name = "core-foundation-sys" @@ -432,19 +521,13 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.2.2" @@ -456,9 +539,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", ] @@ -471,20 +554,21 @@ checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "delharc" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3658b90877f637514897c3dcbd628e0d290bcc7fee551a398b3994ebdc62d51e" +checksum = "9cfdcd2bee8dc367afd9064f3b0849156a4c29fc70d98370dae62773594d04dd" dependencies = [ "bitflags 2.13.2", + "bytemuck", "chrono", "memchr", ] [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" [[package]] name = "des" @@ -509,9 +593,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "encoding_rs" @@ -527,11 +611,46 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "enough" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877362cb7b70d1c617f0cd3c67708bd58d894a86af5104018e2c10ebaee3c02f" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] [[package]] name = "exav" @@ -550,12 +669,13 @@ dependencies = [ name = "exav-core" version = "0.0.2" dependencies = [ - "base64 0.22.1", + "base64", "bit-set", "cab", "cfb", "crc32fast", "exav-imagehash", + "exav-render", "exav-unpack", "exav-x86", "flate2", @@ -589,6 +709,7 @@ name = "exav-grep" version = "0.0.2" dependencies = [ "clap", + "exav-render", "exav-unpack", "regex", "walkdir", @@ -617,20 +738,27 @@ name = "exav-render" version = "0.0.2" dependencies = [ "bytemuck", + "cfb", + "encoding_rs", "exav-unpack", "fax", "flate2", "gif", - "half", "hayro-ccitt", "hayro-jbig2", "hayro-jpeg2000", "image", + "md-5", "png", + "pulldown-cmark", "quick-error", + "quick-xml", "serde_json", + "sha1", "sha2", "weezl", + "zenjxl-decoder", + "zip", "zune-core 0.4.12", "zune-core 0.5.0", "zune-jpeg 0.4.21", @@ -643,7 +771,7 @@ version = "0.0.2" dependencies = [ "aes", "apfs", - "base64 0.22.1", + "base64", "bitstream-io", "byteorder", "bzip2", @@ -659,14 +787,17 @@ dependencies = [ "digest", "encoding_rs", "exav-pe-emu", - "explode", "ext4-view", "fatfs", "flate2", + "hayro-interpret", + "hayro-syntax", "hfsplus", "hmac", + "html5gum", + "kurbo", "lzfse_rust", - "lzma-rust2 0.21.0", + "lzma-rust2", "lznt1", "lzxd", "mail-parser", @@ -684,7 +815,6 @@ dependencies = [ "tar", "thiserror", "tinyvec", - "unshield", "xz4rust", "zip", ] @@ -705,15 +835,6 @@ dependencies = [ "iced-x86", ] -[[package]] -name = "explode" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46695dd0a1b7a2a6c3e0800604bd9738c2699682b9fc325d4886b6c1a78025ac" -dependencies = [ - "arraydeque", -] - [[package]] name = "ext4-view" version = "1.0.0" @@ -724,6 +845,12 @@ dependencies = [ "crc", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fatfs" version = "0.3.6" @@ -764,6 +891,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611" + [[package]] name = "filetime" version = "0.2.29" @@ -776,9 +909,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "flate2" @@ -797,6 +930,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + [[package]] name = "futures-core" version = "0.3.34" @@ -865,17 +1007,6 @@ dependencies = [ "scroll", ] -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -906,12 +1037,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2866f0d976e76497480b853d5e258dab6e4b89ecfe991124e08f15dfa8fd03ad" +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9df074ff163a63864cec9b0bf640ccbff8abf4e44481df7369ce0ac172ffbe" +dependencies = [ + "bitflags 2.13.2", + "hayro-cmap", + "hayro-syntax", + "kurbo", + "moxcms 0.8.1", + "phf", + "rustc-hash 2.1.3", + "siphasher", + "skrifa", + "smallvec", + "yoke", +] + [[package]] name = "hayro-jbig2" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000e025850f4592ddcaee85640db70704151f2bafd43b6f431aa5b6a2c6a35a9" dependencies = [ + "fearless_simd", "hayro-ccitt", ] @@ -920,6 +1080,31 @@ name = "hayro-jpeg2000" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122c5f834c37a2260b9c851bb8be685cf539b2a8b94b704a931ef7d2525d5835" +dependencies = [ + "fearless_simd", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-syntax" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cdc818d8d265dcb42081d488f7599df4f73fb79e9d347ecedcd3c23809d2026" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash 2.1.3", + "smallvec", + "zune-jpeg 0.5.8", +] [[package]] name = "heck" @@ -929,11 +1114,12 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hfsplus" -version = "0.2.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1003d8eed80d61e8e8ea6a958bbe1afd3aef060882b9d6134e50500dd307c0dc" +checksum = "c8dd01379b7ef63f131bcd053ee6acd0fa940cdadec0caae4e3c8d502447fd3d" dependencies = [ "byteorder", + "cmpfs", "thiserror", ] @@ -946,6 +1132,12 @@ dependencies = [ "digest", ] +[[package]] +name = "html5gum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428502d3ec1742c35e015871aef742bc4722a9acfcb616b8ff79b519922e1c36" + [[package]] name = "http" version = "1.5.0" @@ -1015,7 +1207,7 @@ dependencies = [ "color_quant", "gif", "image-webp", - "moxcms", + "moxcms 0.7.11", "num-traits", "png", "qoi", @@ -1043,9 +1235,9 @@ dependencies = [ [[package]] name = "inout" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f" dependencies = [ "block-padding", "hybrid-array", @@ -1059,9 +1251,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -1083,6 +1275,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.1" @@ -1097,9 +1301,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "log" @@ -1109,34 +1313,32 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "logos" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" dependencies = [ - "beef", "fnv", - "lazy_static", "proc-macro2", "quote", + "regex-automata", "regex-syntax", - "rustc_version", "syn 2.0.119", ] [[package]] name = "logos-derive" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" dependencies = [ "logos-codegen", ] @@ -1149,15 +1351,9 @@ checksum = "5edeac3ec657925dececc672bdd58fa2212291886ddcb3e08aa46924c78340c6" [[package]] name = "lzma-rust2" -version = "0.16.5" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" - -[[package]] -name = "lzma-rust2" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b" +checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa" dependencies = [ "sha2", ] @@ -1171,6 +1367,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lzvn-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c51f6215e459d29ee8b61c52074f545fe5518bcb351c590c6e98aec21a5a00" + [[package]] name = "lzxd" version = "0.2.7" @@ -1208,6 +1410,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1238,6 +1449,16 @@ dependencies = [ "pxfm", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multiversion_no_op" version = "1.0.0" @@ -1280,6 +1501,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num-integer" version = "0.1.47" @@ -1310,6 +1542,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.13.0" @@ -1326,6 +1564,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1351,17 +1631,26 @@ dependencies = [ "miniz_oxide 0.8.9", ] +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppmd-rust" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1" +checksum = "cd4acc8d264ea385db64ccfdcdfd5909ee9eda0975666723d59f3bfec80b3611" [[package]] name = "primal-check" @@ -1372,6 +1661,37 @@ dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1381,6 +1701,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.2", + "memchr", + "unicase", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -1404,9 +1735,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] @@ -1451,6 +1782,17 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "read-fonts" +version = "0.43.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005c8acf251756c478b0bf402885bfd88a1476020c4c7e6060edc1aa68da38ea" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + [[package]] name = "regex" version = "1.13.1" @@ -1515,12 +1857,13 @@ dependencies = [ [[package]] name = "rowan" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +checksum = "14b574c58582fa59fa43a2feb6608b8744184659f08a2e0117e4b8224d95ed61" dependencies = [ "countme", "hashbrown 0.14.5", + "memoffset", "rustc-hash 1.1.0", "text-size", ] @@ -1537,15 +1880,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustdct" version = "0.7.1" @@ -1571,9 +1905,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -1595,9 +1929,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -1612,12 +1946,15 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ruzstd" -version = "0.8.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" -dependencies = [ - "twox-hash", -] +checksum = "69e4ce4a039f42bf25920b95667e4d789e98749ab11bbb8d2c2936f788421bb5" + +[[package]] +name = "safe_unaligned_simd" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c874fb993656d484725d58fbfddb39dfcf98ebcadc1a1d09568aa6b39bafba" [[package]] name = "salzweg" @@ -1657,15 +1994,9 @@ checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.229" @@ -1693,14 +2024,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -1711,14 +2042,14 @@ dependencies = [ [[package]] name = "sevenz-rust2" -version = "0.20.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29225600349ef74beda5a9fffb36ac660a24613c0bde9315d0c49be1d51e9c24" +checksum = "2f0c68c0e8289bfd09e49ef433e8de02c7c464afc1d7f5a275c99749a880e301" dependencies = [ "bzip2", "crc32fast", "js-sys", - "lzma-rust2 0.16.5", + "lzma-rust2", "ppmd-rust", "wasm-bindgen", ] @@ -1736,9 +2067,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures", @@ -1763,12 +2094,40 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.46.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4febebda507fb889c545a37a135517769d1391941013561292897961d1887d6" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strength_reduce" version = "0.2.4" @@ -1811,15 +2170,26 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "tar" version = "0.4.46" @@ -1853,7 +2223,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1887,6 +2257,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7184dfffa0d05e10284df327c6cb52f0d62c5aa6c9a8b9236fa151d4361882f1" +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "transpose" version = "0.2.3" @@ -1897,17 +2297,11 @@ dependencies = [ "strength_reduce", ] -[[package]] -name = "twox-hash" -version = "2.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" - [[package]] name = "typed-path" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c" [[package]] name = "typenum" @@ -1915,6 +2309,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -1923,15 +2323,15 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" dependencies = [ "tinyvec", ] @@ -1942,16 +2342,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unshield" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd29bc2ffd8a674b7de402169d86b3c083229373d05c388526d626408416c5c" -dependencies = [ - "byteorder", - "explode", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -1960,11 +2350,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.4.2" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b" dependencies = [ - "base64 0.23.1", + "base64", "flate2", "log", "percent-encoding", @@ -1977,11 +2367,11 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" dependencies = [ - "base64 0.23.1", + "base64", "http", "httparse", "log", @@ -2001,9 +2391,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.1" +version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ "js-sys", "wasm-bindgen", @@ -2057,7 +2447,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -2082,9 +2472,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] @@ -2104,6 +2494,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winarm-cpufeatures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d107c80dd656e1a2c254dfb1d57ece73b7f515b8a8684c270c23cab68f4a440d" + [[package]] name = "windows-core" version = "0.62.2" @@ -2245,6 +2641,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "xz4rust" version = "0.2.3" @@ -2253,9 +2658,9 @@ checksum = "606930b71bad6379f366994e4e6252a3462fa28bfc3695c33b07bef8f967d6d6" [[package]] name = "yara-x-parser" -version = "1.19.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5564d72253142774903eafc8f4c92bebef40d65c176a11d351258e80cc8a1c7" +checksum = "89313553cc4cf8961c257d282c902da1d5de568cd656b39962bdf9edd1e3935e" dependencies = [ "ascii_tree", "bitflags 2.13.2", @@ -2269,36 +2674,103 @@ dependencies = [ ] [[package]] -name = "zerocopy" -version = "0.8.59" +name = "yoke" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ - "zerocopy-derive", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "zerocopy-derive" -version = "0.8.59" +name = "yoke-derive" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zenjxl-decoder" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d68502daf17b161b6fb73657f4a4c60ba5a0491846aae769e33b34dc5ec513" +dependencies = [ + "aligned-vec", + "array-init", + "atomic_refcell", + "bytemuck", + "byteorder", + "enough", + "num-derive", + "num-traits", + "paste", + "smallvec", + "thiserror", + "zenjxl-decoder-macros", + "zenjxl-decoder-simd", +] + +[[package]] +name = "zenjxl-decoder-macros" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d5f705a56b0a01d283503fbe7d5e3b3e51d7321b5f1bba7b07bdeea7e2c6f3d" +dependencies = [ + "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.119", ] +[[package]] +name = "zenjxl-decoder-simd" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237f3740d3d098854c6008515cdff29d8cbadcb6b4e3a1582e28dd3a04e3213e" +dependencies = [ + "archmage", + "paste", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zip" -version = "9.0.1" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "bitstream-io", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 8ab9bf7..12f1c33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,15 @@ repository = "https://github.com/sylvinus/exav" homepage = "https://exav.org" # Checked by `make msrv` rather than asserted: a floor nothing verifies drifts. # -# The number is set by dependencies, not by this code: exav-render's image -# decoders hayro-jpeg2000 0.4, hayro-jbig2 0.3.1 and hayro-ccitt 0.4 declare -# 1.92 (and `yara-x-parser` 1.91), so cargo refuses to build them on anything -# older. exav's own source needs only 1.88 (`slice::as_chunks`, +# The number is set by dependencies, not by this code: exav-unpack's LHA reader +# delharc 0.8 declares 1.95 (if-let guards), `sevenz-rust2` 0.24 1.93, +# exav-render's image decoders hayro-jpeg2000 0.4, hayro-jbig2 0.3.1 and +# hayro-ccitt 0.4 1.92, and `yara-x-parser` 1.91, so cargo refuses to build them +# on anything older. exav's own source needs only 1.88 (`slice::as_chunks`, # `is_multiple_of`, and the let-chains the `zip` dependency uses). Worth # rechecking when those dependencies move; lowering it means confirming with # `make msrv`, not editing this line. -rust-version = "1.92" +rust-version = "1.95" # crates.io discovery. keywords: max 5, <=20 chars each. categories must be from # https://crates.io/category_slugs (an invalid one fails `cargo publish`). keywords = ["antivirus", "clamav", "malware", "scanner", "security"] @@ -28,7 +29,7 @@ categories = ["command-line-utilities", "filesystem"] # `env` lets every flag name the environment variable it falls back to, so the # precedence (flag, then environment, then default) is one rule the parser # applies and `--help` prints, rather than per-flag code. -clap = { version = "4.5", features = ["derive", "env"] } +clap = { version = "4.6", features = ["derive", "env"] } walkdir = "2.5" # Catch integer overflow in release too: on hostile input a silent wraparound diff --git a/Dockerfile b/Dockerfile index e6c653a..7f6c4d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,9 +38,9 @@ ARG BIN_SOURCE=build # of magnitude slower than cross-compiling it. # rust:alpine targets *-unknown-linux-musl and links statically. `build-base` # provides the C toolchain the TLS stack (ring, via the `http` feature's ureq → -# rustls) needs to compile under musl. Pinned to 1.92, the MSRV floor (see the +# rustls) needs to compile under musl. Pinned to 1.95, the MSRV floor (see the # workspace `rust-version`, which says what sets it); bump both together. -FROM rust:1.92-alpine AS build +FROM rust:1.95-alpine AS build RUN apk add --no-cache musl-dev build-base WORKDIR /src COPY . . diff --git a/Makefile b/Makefile index d235de8..602f981 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DBDIR ?= exav-db EXAVDB ?= exav.exavdb .DEFAULT_GOAL := build -.PHONY: build release test test-native test-yara-diff test-wasm test-js test-viewer test-www test-helm wasm-sizes lint fmt msrv av-audit miri publish-check publish fuzz db exavdb cache daily clean www-dev www-build help +.PHONY: build release test test-native test-yara-diff test-wasm test-js test-viewer test-www test-formats formats test-helm wasm-sizes lint fmt msrv av-audit miri publish-check publish fuzz db exavdb cache daily clean www-dev www-build help ## build: compile the release binary build release: @@ -71,7 +71,32 @@ test-native: $(CARGO) test -p exav-render --no-default-features --features dwg # IFC and STL, behind `ifc` and `stl`, as @exav/viewer's model module # builds them. - $(CARGO) test -p exav-render --no-default-features --features ifc,stl + $(CARGO) test -p exav-render --no-default-features --features ifc,stl,obj,ply,gltf + # Markdown, behind `markdown`, as @exav/viewer's text module builds it. + $(CARGO) test -p exav-render --no-default-features --features markdown + # The office converters (spreadsheets, presentations, text documents), + # behind `office-*`, as @exav/viewer's sheets, slides and docs modules + # build them: each alone, since slides compile part of sheets, then all + # of them with the test writers the integration tests build documents with. + $(CARGO) test -p exav-render --no-default-features --features office-sheets + $(CARGO) test -p exav-render --no-default-features --features office-slides,office-test-util + $(CARGO) test -p exav-render --no-default-features --features office-docs,office-test-util + $(CARGO) test -p exav-render --no-default-features --features office,office-test-util + # The text a reader sees, behind `text` (HTML, Office XML parts, drawings) + # and `text-pdf` (PDF, through hayro, which switches flate2 to zlib-rs: its + # own pass, so that the default passes do not run with it). + $(CARGO) test -p exav-unpack --features text + $(CARGO) test -p exav-render --no-default-features --features text + $(CARGO) test -p exav-unpack --features text-pdf --test all text:: + $(CARGO) test -p exav-render --no-default-features --features text-pdf + $(CARGO) test -p exav-core --features visible-text-pdf --test all visible_text + $(CARGO) test -p exav-grep --features text-pdf --test text + # The office formats' text (DOC, XLS, PPT, RTF, flat OpenDocument, iWork...), + # behind `text-office`. + $(CARGO) test -p exav-render --no-default-features --features text-office,office-test-util + $(CARGO) test -p exav-core --features visible-text-office --test all visible_text + $(CARGO) test -p exav-grep --features text-office --test text --test text_office + $(CARGO) test -p exav --features visible-text-office --test office_text ## test-yara-diff: the yara-x A/B differential harness. Compiles the SAME rules ## with both engines and asserts equal matching-rule sets. NOT @@ -119,6 +144,19 @@ test-viewer: test-www: ./scripts/test-www.sh +## formats: write the pages generated from the format registry +## (formats/registry.json): the format matrix and the viewer's +## summary sentence in the READMEs and the site. +formats: + node scripts/formats.mjs generate + +## test-formats: the format registry against the extractor's formats, the +## scanner's file types, the viewer's plugins, and the pages +## generated from it (test-www runs it too; the viewer's and the +## scanner's own registry tests are in test-viewer and test-native). +test-formats: + node scripts/formats.mjs check + ## test-helm: render-check the Helm chart (render guards, hardening settings, ## packaging). Needs `helm` on PATH, no cluster. CI runs `ct lint` ## in addition. See scripts/test-helm.sh. @@ -141,7 +179,18 @@ lint: $(CARGO) clippy --all-targets --features exav-core/http -- -D warnings $(CARGO) clippy -p exav-core --all-targets --features unstable-internals -- -D warnings $(CARGO) clippy -p exav-render --all-targets --features dwg -- -D warnings - $(CARGO) clippy -p exav-render --all-targets --no-default-features --features ifc,stl -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features ifc,stl,obj,ply,gltf -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features markdown -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features text-pdf -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features office-sheets -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features office-slides -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features office-docs -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features office,office-test-util -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features -- -D warnings + $(CARGO) clippy -p exav-render --all-targets --no-default-features --features text-office,office-test-util -- -D warnings + $(CARGO) clippy -p exav-core --all-targets --features visible-text-office -- -D warnings + $(CARGO) clippy -p exav-grep --all-targets --features text-office -- -D warnings + $(CARGO) clippy -p exav-unpack --all-targets --features text-pdf -- -D warnings ## fmt: format the code fmt: diff --git a/NOTICE b/NOTICE index a0fb856..f3888ed 100644 --- a/NOTICE +++ b/NOTICE @@ -74,7 +74,12 @@ Copyright (c) 2024. The YARA-X Authors. All Rights Reserved. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Other BSD-3-Clause components linked by exav (same 3-clause terms above, with -their respective copyright holders): subtle, tlsh2. +their respective copyright holders): subtle, tlsh2, and, in exav-render's `jxl` +feature (JPEG XL in @exav/viewer's image module), zenjxl-decoder with its +zenjxl-decoder-simd and zenjxl-decoder-macros: "Copyright (c) the JPEG XL +Project Authors. All rights reserved." It is a fork of the libjxl team's +jxl-rs (https://github.com/libjxl/jxl-rs), built `forbid(unsafe_code)` and +without its SIMD. ------------------------------------------------------------------------------- @@ -255,13 +260,18 @@ Copyright (c) Mike Krüger. ------------------------------------------------------------------------------- -1h1. unshield / explode, MIT (linked dependency + test fixtures) +1h1. unshield / explode, MIT, and blast.c, zlib (source derivatives + test + fixtures) ------------------------------------------------------------------------------- -crates/exav-unpack/src/formats/ishield_z.rs reads InstallShield `.z` archives -through the `unshield` crate, whose only real dependency is `explode`, a leaf -decoder for the PKWARE Data Compression Library "implode" codec those archives -use. Both MIT. +crates/exav-unpack/src/formats/ishield_z.rs reads the table of contents of +InstallShield `.z` archives as the `unshield` crate 0.2.0 does, and +crates/exav-unpack/src/formats/dcl_explode.rs decodes their PKWARE Data +Compression Library "implode" members as the `explode` crate 0.1.2 does. Both +crates are MIT. `explode` is a port of Mark Adler's `blast.c` (zlib +`contrib/blast`, zlib licence, text in section 1l; Copyright (C) Mark Adler), +whose algorithm and code-length tables dcl_explode.rs keeps; it is altered +from both: it decodes a whole slice into a capped buffer. The fixtures in crates/exav-unpack/tests/fixtures/ishieldz/ are that project's own examples, redistributed under the same terms. `undhr.z` is shipped alongside @@ -360,6 +370,44 @@ Copyright © 1995-2011 by Fredrik Lundh and contributors Copyright © 2010 by Jeffrey 'Alex' Clark and contributors +------------------------------------------------------------------------------- +1h6. Apple lzfse, BSD-3-Clause (source derivative) +------------------------------------------------------------------------------- + +crates/exav-unpack/src/formats/lzfse.rs, the LZFSE and LZVN decoder for DMG +runs, is a Rust port of the decoder of Apple's reference implementation +(lzfse_decode_base.c, lzfse_fse.c/.h, lzvn_decode_base.c and the tables of +lzfse_internal.h), changed to decode a whole buffer into a capped one. + +Upstream: https://github.com/lzfse/lzfse +Copyright (c) 2015-2016, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ------------------------------------------------------------------------------- 1h. oletools test sample, BSD-2-Clause (test fixture) ------------------------------------------------------------------------------- @@ -461,6 +509,9 @@ Copyright (c) Nullsoft and contributors. 3. This notice may not be removed or altered from any source distribution. +The zlib-rs crate (the `text-pdf` features' DEFLATE) is under the same +Zlib terms: (C) 2024 Trifecta Tech Foundation. + ------------------------------------------------------------------------------- 1m. sevenz-rust2, Apache-2.0 (design reference) @@ -494,6 +545,153 @@ Open Font License 1.1. NOTICE.md beside them gives each face's copyright and version, and OFL-1.1.txt the licence. +------------------------------------------------------------------------------- +1o. numbers-parser data (MIT) and the OpenOffice.org Excel format documentation +------------------------------------------------------------------------------- + +These are the office converters of exav-render (the `office-sheets`, +`office-slides` and `office-docs` features, `src/formats/office/`). They reach +the @exav/viewer npm package and, with `text-office`, exav-grep; the default +exav binary contains none of them. Their full notice is +crates/exav-render/NOTICE. + +They are written from published specifications: Microsoft's Open +Specifications ([MS-XLS], [MS-PPT], [MS-DOC], [MS-CFB], [MS-ODRAW], +[MS-OSHARED], [MS-OFFCRYPTO], [MS-VSDX]) and Visio file format reference, +ECMA-376 and ISO/IEC 29500, OpenDocument 1.2 and 1.3 (OASIS), the RTF +specification, Apple's Technical Notes TN2067 and TN2073 (Keynote 1) and RFC +1741 (BinHex 4.0). OpenOffice.org's "Microsoft Excel File Format" documentation +(Public Documentation License) gives the BIFF2 to BIFF5 record layouts. + +Two projects supply data: numbers-parser (https://github.com/masaccio/numbers-parser, +MIT, Copyright 2021 Jon Connell) gave Numbers' function indexes, the families +of macOS fonts' PostScript names and the layout of table records; keynote-parser +(https://github.com/psobot/keynote-parser, MIT, Copyright 2019-2025 Peter +Sobot) gave the Keynote and Pages message layout, and its sample.key and +table.key are test fixtures. + +The readers for PowerPoint 95 and Keynote 2 to 5 follow no published +description: they were written from sample documents, with the output of other +office applications on them used as a black box (rendered pages, never source). +No source of another office application or of a document-format library was +read. + + +------------------------------------------------------------------------------- +1p. Camera RAW previews (exav-render `raw`): specifications and sample files +------------------------------------------------------------------------------- + +crates/exav-render/src/formats/image/raw.rs and raw/ read the previews of +camera RAW files. No code was taken from another implementation. They were +written from these public specifications, which are not redistributed here: + +- TIFF Revision 6.0 (Adobe Systems Incorporated, 1992), published by Adobe for + anyone to implement. +- Adobe Digital Negative (DNG) Specification 1.7.1.0 (Adobe), published with a + royalty-free licence to implement DNG. +- Exif 2.32, CIPA DC-008-2019 (Camera & Imaging Products Association), freely + downloadable. +- ITU-T T.81 | ISO/IEC 10918-1, the JPEG standard. +- ISO/IEC 14496-12, ISO base media file format, from ISO's publicly available + standards. + +What the makers' own containers hold (the signatures of RAF, X3F, CRW, MRW, +ORF, RW2 and IIQ, the compression numbers of their sensor data) was read off +sample files from https://raw.pixls.us, which its contributors dedicate to the +public domain under CC0 1.0 +(https://creativecommons.org/publicdomain/zero/1.0/), with tools written for +the purpose. None of those files is in this repository; the tests use the +synthetic containers of crates/exav-render/tests/fixtures/raw/make.py. + +The Canon CR3 reader (raw/bmff.rs and the CR3 parts of raw.rs and make.py) was +written from ISO/IEC 14496-12 and the Exif and TIFF specifications above, and +from 110 CR3 sample files of the same CC0 source, which a box lister written +for the purpose walked: the box layout (the `crx ` brand of `ftyp`, the `uuid` +boxes of `moov`, the box that holds the Exif of the first directory and its +orientation, the JPEGs the file embeds and where) was read off those files +alone. No CR3 documentation or source code of any other project was read: no +format description, no README, no tag table, and no other RAW reader or +converter. + + +------------------------------------------------------------------------------- +1q. Example files of the @exav/viewer demo +------------------------------------------------------------------------------- + +The @exav/viewer demo lists example files kept in +crates/exav-viewer/demo/examples/. Most are made by scripts of this repository +and given to the public domain (CC0-1.0). The others are public domain, CC0, +CC-BY, MIT, BSD or Apache files, each credited with its author, its +source and the page that states its licence in examples.json, NOTICE.md and +https://exav.org/viewer/examples/. The small ones are committed; the rest are +downloaded by `npm run demo:examples` from pinned addresses and checked against +their SHA-256. They are test inputs only and are not part of any published +package. + + +------------------------------------------------------------------------------- +1r. IFC geometry (exav-render `ifc`): specifications +------------------------------------------------------------------------------- + +The curved-face, sweep-parameter, alignment, grid and storey code is written +from buildingSMART International's published IFC4 and IFC4X3 EXPRESS schemas +and documentation (CC BY-ND 4.0, read, not copied) and the surface and curve +definitions of ISO 10303-42. No code from another IFC library was read. + + +------------------------------------------------------------------------------- +1s. yoke, yoke-derive, zerofrom, zerofrom-derive, Unicode License v3 +------------------------------------------------------------------------------- + +These crates (the ICU4X project's, linked through hayro-interpret, the PDF +interpreter of the `text-pdf` features) carry this licence; reproduce its +notice in distributions. + + UNICODE LICENSE V3 + + COPYRIGHT AND PERMISSION NOTICE + + Copyright © 2020-2024 Unicode, Inc. + + NOTICE TO USER: Carefully read the following legal agreement. BY + DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR + SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE + TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT + DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of data files and any associated documentation (the "Data Files") or + software and any associated documentation (the "Software") to deal in the + Data Files or Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, and/or sell + copies of the Data Files or Software, and to permit persons to whom the + Data Files or Software are furnished to do so, provided that either (a) + this copyright and permission notice appear with all copies of the Data + Files or Software, or (b) this copyright and permission notice appear in + associated Documentation. + + THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + THIRD PARTY RIGHTS. + + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA + FILES OR SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall + not be used in advertising or otherwise to promote the sale, use or other + dealings in these Data Files or Software without prior written + authorization of the copyright holder. + + Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. + ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines + Corporation and others. + + ------------------------------------------------------------------------------- 2. ISC-licensed components ------------------------------------------------------------------------------- @@ -520,11 +718,23 @@ the distributor's choice. Notable examples: clap CLI parsing sha1, sha2, md-5, digest (RustCrypto) hashing flate2, miniz_oxide DEFLATE / gzip / zlib - lzfse_rust, delharc, lzxd DMG/LHA/CAB codecs + delharc, lzxd LHA/CAB codecs bitstream-io bit-level readers for decoders stringprep PDF password normalization ext4-view ext2/3/4 filesystem reader image image decoding (image hashing) + pulldown-cmark Markdown in exav-render (`markdown` + feature) and @exav/viewer; MIT, + 2015 Google Inc. + html5gum HTML text in exav-unpack (`text` + feature): the tokenizer; MIT, + 2021 Markus Unterwaditzer + hayro-interpret, hayro-syntax, PDF text in exav-unpack (`text-pdf` + hayro-cmap, hayro-postscript feature, off by default): Apache-2.0 + OR MIT, Laurenz Stampfl + phf glyph-name tables in hayro; MIT, + 2014-2022 Steven Fackler, Yuki + Okushi hayro-jpeg2000, hayro-jbig2, JPEG 2000, JBIG2 and CCITT fax in hayro-ccitt exav-render (image hashing) and @exav/viewer; hayro-jpeg2000's @@ -536,16 +746,17 @@ the distributor's choice. Notable examples: wasm-bindgen, js-sys, web-sys, WebAssembly/JS bindings; these reach wasm-bindgen-futures the published npm artifact -sevenz-rust2, bzip2 and ppmd-rust are dev-only: they build test fixtures. The -shipped 7z reader is native (§1m) and the bzip2 decoder is vendored (§1h3). +sevenz-rust2, bzip2, ppmd-rust and lzfse_rust are dev-only: they build test +fixtures. The shipped 7z reader is native (§1m), the bzip2 decoder is vendored +(§1h3) and the LZFSE decoder is ported from Apple's (§1h6). MIT-only components (the MIT copyright and permission notice must be retained; the MIT text is in the exav LICENSE file): apfs, ascii_tree, cfb, color_quant, -deflate64, explode, fatfs, fax, fax_derive, goblin, hfsplus, lznt1, quick-xml, +deflate64, fatfs, fax, fax_derive, goblin, hfsplus, lznt1, quick-xml, rmp, rmp-serde, ruzstd, salzweg, scroll, scroll_derive, simd-adler32, slab, -strsim, twox-hash, unshield, xz4rust, zip, zmij, and bytes (`http` features -only). fancy-regex and tiff are vendored, and Pillow's resampler ported -(§1h5). +strsim, xz4rust, zip, zmij, and bytes (`http` features only). fancy-regex and +tiff are vendored, and Pillow's resampler ported (§1h5); unshield and explode +are ported (§1h1). Apache-2.0-only: lzma-rust2 (it ships no NOTICE file of its own; the license text is in section 4). @@ -555,8 +766,9 @@ BSD-3-Clause: yara-x-parser (§1), and subtle (`http` features only). tlsh2 is Apache-2.0 OR BSD-3-Clause. RustCrypto crates are MIT OR Apache-2.0. Offered under Zlib among other -licenses: miniz_oxide, tinyvec, bytemuck. Unicode-3.0: unicode-ident -(build-time). Other permissive +licenses: miniz_oxide, tinyvec, bytemuck. Zlib: zlib-rs (§1l). Unicode-3.0: +unicode-ident (build-time), and yoke, yoke-derive, zerofrom and +zerofrom-derive (§1s). Other permissive licenses: CC0-1.0 OR MIT-0 OR Apache-2.0 (constant_time_eq), CDLA-Permissive-2.0 (webpki-roots, TLS root store, `http` features only). The dev-only crates add CC0-1.0 / MIT-0 (ppmd-rust) and bzip2-1.0.6 diff --git a/README.md b/README.md index 61720a7..4532278 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,7 @@ cargo install exav-unpack ## File viewer: `@exav/viewer` -A file viewer for the browser: PDF, images, DWG and DXF drawings, Office -documents, IFC and STL models, media and archives. Each file can open in a +A file viewer for the browser: PDF, images, DWG and DXF drawings, Word, Excel, PowerPoint and CSV, text (Markdown, JSON, XML, source code), IFC, STL, OBJ, PLY and glTF models, video, audio, and what is inside an archive. Each file can open in a sandboxed frame of its own, and exav's own decoders run in WebAssembly. ```sh @@ -62,7 +61,7 @@ npm install @exav/viewer |---|---| | [`exav-grep`](crates/exav-grep/README.md) | `grep`, but it can see inside archives | | [`exav-core`](crates/exav-core/README.md) | The scanning engine, as a Rust library | -| [`exav-render`](crates/exav-render/README.md) | The decoders behind the viewer: images, DWG, DXF, IFC, STL | +| [`exav-render`](crates/exav-render/README.md) | The decoders behind the viewer: images, DWG, DXF, IFC, STL, OBJ, PLY, glTF, Office documents | | [`exav-imagehash`](crates/exav-imagehash/README.md) | Perceptual image hashes, ClamAV's `fuzzy_img` among them | | [`exav-pe-emu`](crates/exav-pe-emu/README.md) | A bounded x86-32 emulator that runs packer stubs to unpack them | | [`exav-x86`](crates/exav-x86/README.md) | A decode-only x86-32 instruction decoder | diff --git a/crates/exav-core/Cargo.toml b/crates/exav-core/Cargo.toml index ef9104f..49e111d 100644 --- a/crates/exav-core/Cargo.toml +++ b/crates/exav-core/Cargo.toml @@ -32,8 +32,9 @@ memchr = "2" md-5 = "0.11" sha1 = "0.11" sha2 = "0.11" -# executable parsing (PE/ELF/Mach-O) -goblin = "0.10" +# PE and ELF parsing. Without Mach-O, `ar` archives and TE images, which exav +# reads with its own code or not at all: fewer of goblin's parsers compiled. +goblin = { version = "0.10", default-features = false, features = ["std", "elf32", "elf64", "pe32", "pe64"] } # x86 instruction decoder for the bytecode disasm_x86 API. Decode-only, in the # workspace, and dependency-free: a general-purpose decoder covers the whole ISA # at the cost of hundreds of `unsafe` blocks, which is a poor trade for one API. @@ -50,21 +51,26 @@ tlsh2 = { version = "1", features = ["diff"] } # formats past the five clamscan treats as graphics, JPEG 2000 and JBIG2 # included, are hashed except under `--clamav-compat`. exav-imagehash = { path = "../exav-imagehash", version = "0.0.2", default-features = false, features = ["webp", "ico", "pnm", "qoi", "dds", "ff", "hdr", "jp2", "jbig2"], optional = true } +# The text of a document as a reader sees it (`visible-text` feature): +# exav-render's `text`: Markdown (pulldown-cmark without its `html` and `simd` +# features), HTML (html5gum's tokenizer), Office and OpenDocument XML parts, +# PDF (hayro) and drawings. +exav-render = { path = "../exav-render", version = "0.0.2", default-features = false, features = ["text"], optional = true } # HTTP(S) range-request backend (optional) ureq = { version = "3", optional = true } # Anchor-frequency counting at database build: tens of millions of u64 gram # keys, where the hash is the whole cost of the lookup. rustc-hash = "2" -serde = { version = "1.0.228", features = ["derive"] } +serde = { version = "1.0.229", features = ["derive"] } rmp-serde = "1.3.1" # The `.exavdb` binary blobs, written and read directly (see `database::enc_bytes`). rmp = "0.8.15" -regex = "1.12.4" +regex = "1.13.1" # The backtracking engine for the PCRE subsignatures `regex` cannot run # (lookaround, backreferences, atomic groups) is fancy-regex, vendored in # `src/fancy_regex/` (its README says why). It needs this beside `regex-automata` # and `regex-syntax` below. -bit-set = { version = "0.8", default-features = false, features = ["std"] } +bit-set = { version = "0.11", default-features = false, features = ["std"] } # YARA rule support (ClamAV loads .yar/.yara by default, so a drop-in must too), # via exav's native YARA engine under `src/yara/`. It compiles rules to a native # tree-walking evaluator (no runtime codegen at scan time, W^X preserved) and @@ -75,7 +81,7 @@ bit-set = { version = "0.8", default-features = false, features = ["std"] } # # Parser + AST only. The AST borrows the source, so the compiler lowers it into # an owned IR. -yara-x-parser = { version = "1.18", optional = true } +yara-x-parser = { version = "1.19", optional = true } # Byte-oriented regex engine with anchored search: YARA regexp patterns, the # `matches` operator and hex patterns (lowered to byte regexes), and the lazy # DFAs that run a regex over an object read through the block cache. @@ -87,14 +93,30 @@ regex-automata = "0.4" # match is ever missed). regex-syntax = "0.8" # base64 alphabet handling for the `base64`/`base64wide` string modifiers. -base64 = { version = "0.22", optional = true } +base64 = { version = "0.23", optional = true } # The database file's checksum, and crc32/checksum32 for YARA's `hash` module. crc32fast = "1" # JSON output for the WASI binary (`wasi-bin`), nothing else. -serde_json = { version = "1.0.140", optional = true } +serde_json = { version = "1.0.154", optional = true } [features] -default = ["yara", "all-formats", "decrypt", "dlp", "phishing", "image-hash"] +default = ["yara", "all-formats", "decrypt", "dlp", "phishing", "image-hash", "visible-text"] +# A document's visible text (its words, and where each link leads) is matched +# as a text view, as well as its bytes: Markdown, HTML, Office and OpenDocument +# XML parts, PDF and drawings. Not under `--clamav-compat`. +visible-text = ["dep:exav-render"] +# And PDF's, read by hayro. Not a default: hayro-interpret turns on +# hayro-syntax's `unsafe` feature, which switches every `flate2` in the build to +# zlib-rs (and changes what a damaged stream gives back) and adds SIMD to the +# image decoders; it also adds some 40 crates. See docs/DEPENDENCIES.md. +visible-text-pdf = ["visible-text", "exav-render/text-pdf"] +# And the office formats the XML parts do not reach: DOC, XLS, PPT, RTF, Word +# 2003 XML, SpreadsheetML 2003, SYLK, flat OpenDocument, Numbers, Pages and +# Keynote, from exav-render's converters (`office`). Safe Rust and within the +# scan's limits, but the largest readers of the scan: not a default. A document +# of those formats whose text cannot be read (encrypted, past a limit, damaged) +# marks the scan incomplete. +visible-text-office = ["visible-text", "exav-render/text-office"] # `fuzzy_img#` subsignatures, through exav-imagehash and its image decoders. # Without it they load as unsupported, and no image is ever decoded. image-hash = ["dep:exav-imagehash"] @@ -235,18 +257,20 @@ zip = { version = "9", default-features = false, features = ["deflate-flate2"] } # into the build at all. flate2 = "1" tar = { version = "0.4", default-features = false } -cfb = "0.14" +cfb = "0.15" # Writes the MSZIP cabinets the damaged-member tests break. cab = "0.6.0" # only for the base64-embedded-executable end-to-end test -base64 = "0.22" +base64 = "0.23" +# Reads formats/registry.json, which the detection is checked against. +serde_json = "1.0.154" # The reference PPMd8 encoder, for the ZIP method-98 end-to-end test. ppmd-rust = "1" # Encodes the images the `fuzzy_img#` tests scan. image = { version = "=0.25.9", default-features = false, features = ["png", "webp"] } # The unit tests of the vendored `src/fancy_regex/`, as upstream wrote them. matches = "0.1.10" -quickcheck = { version = "1.0", default-features = false } +quickcheck = { version = "1.1", default-features = false } # Small chunks when reading a source, so tests on small inputs cross many # chunk seams. Features only: which formats are built stays with the # dependency above. diff --git a/crates/exav-core/src/engine/mod.rs b/crates/exav-core/src/engine/mod.rs index f95e442..f495798 100644 --- a/crates/exav-core/src/engine/mod.rs +++ b/crates/exav-core/src/engine/mod.rs @@ -432,6 +432,53 @@ pub fn reset_scan_truncated() { SCAN_TRUNCATED.with(|c| c.set(false)); SCAN_OVER_SIZE.with(|r| r.borrow_mut().take()); SCAN_CAPPED.with(|r| r.borrow_mut().take()); + CLEAN_SEEN.with(|m| m.borrow_mut().clear()); +} + +/// Most objects remembered as scanned clean in one scan. +const CLEAN_SEEN_MAX: usize = 4096; + +thread_local! { + /// Objects this scan has scanned to the end and found clean, by a digest of + /// their bytes and of everything about where they sit that a scan reads, + /// with the shallowest depth one was scanned at. Cleared with the + /// truncation flags, at the start of each top-level scan. + static CLEAN_SEEN: RefCell> = + RefCell::new(std::collections::HashMap::new()); +} + +/// Whether an object with this digest was already scanned clean at `depth` or +/// shallower. Deeper copies are not covered: the walk has fewer levels left +/// there, so the same bytes could be reported over the limit. +pub(crate) fn clean_seen(key: &[u8; 32], depth: u32) -> bool { + let seen = CLEAN_SEEN.with(|m| m.borrow().get(key).is_some_and(|&d| d <= depth)); + #[cfg(test)] + if seen { + CLEAN_SKIPS.with(|c| c.set(c.get() + 1)); + } + seen +} + +#[cfg(test)] +thread_local! { + /// How many objects [`clean_seen`] let a scan skip on this thread. + pub(crate) static CLEAN_SKIPS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// Remember an object scanned clean at `depth`. +pub(crate) fn clean_seen_insert(key: [u8; 32], depth: u32) { + CLEAN_SEEN.with(|m| { + let mut m = m.borrow_mut(); + if m.len() < CLEAN_SEEN_MAX || m.contains_key(&key) { + m.entry(key).and_modify(|d| *d = (*d).min(depth)).or_insert(depth); + } + }); +} + +/// The container types above the object being scanned, outermost first, as +/// `Intermediates:` reads them. +pub(crate) fn ancestry_snapshot() -> Vec> { + ANCESTRY.with(|a| a.borrow().clone()) } /// Record that carving reached its cap with more to carve, and why. Unlike a diff --git a/crates/exav-core/src/jsnorm.rs b/crates/exav-core/src/jsnorm.rs index 8311967..2e11fa1 100644 --- a/crates/exav-core/src/jsnorm.rs +++ b/crates/exav-core/src/jsnorm.rs @@ -22,11 +22,10 @@ //! purely static rewriter: it never executes code, so runtime-assembled payloads //! (numeric-array VMs, dynamic dispatch) are intentionally out of scope. //! -//! Not yet implemented (tracked in tmp/GAPS.md): -//! * **Microsoft Script Encoder** (`#@~^…^#~@`, JScript.Encode/VBScript.Encode) -//! decoding — needs the exact published 119×3 translation + 64-entry pick -//! tables; deferred rather than ship an unverified table (and near-absent in -//! the current corpus). +//! Not done here: Microsoft Script Encoder (`#@~^…^#~@`) blobs are decoded by +//! exav-unpack's `screnc`, which surfaces the plaintext as a member of its own. +//! +//! Not yet implemented: //! * **base64→script rescan** — decoding long base64 runs to *script/text* (not //! just exec-magic payloads, which `exav-unpack::base64_payloads` already //! covers). The higher-yield lever per corpus evidence, but needs a distinct diff --git a/crates/exav-core/src/lib.rs b/crates/exav-core/src/lib.rs index eab32a2..86cd5f9 100644 --- a/crates/exav-core/src/lib.rs +++ b/crates/exav-core/src/lib.rs @@ -1829,7 +1829,7 @@ fn visit_member( // of it is a real Clean: exav scans for malware, it is not a // file-integrity validator. Undecodable bytes still present // keep the not-fully-scanned verdict. - let o = member_content_scan(cx, tally, &data, budget, findings, sink); + let o = member_content_scan(cx, tally, &meta.name, &data, budget, findings, sink); if o.is_none() && unpack::decode_error_hides_content(&e) { tally.unscannable.get_or_insert_with(|| { format!( @@ -1848,10 +1848,10 @@ fn visit_member( unpack::volume::Offer::Held => return None, unpack::volume::Offer::PassThrough { data, .. } => data, }; - member_content_scan(cx, tally, &data, budget, findings, sink) + member_content_scan(cx, tally, &meta.name, &data, budget, findings, sink) } Body::Spilled(spilled) => { - let o = member_content_scan(cx, tally, &spilled, budget, findings, sink); + let o = member_content_scan(cx, tally, &meta.name, &spilled, budget, findings, sink); if let Some(e) = spilled.read_error() { tally .unscannable @@ -1957,7 +1957,7 @@ fn walk_members( break; } let _mpg = MatchPathGuard::enter(&name); - terminal = member_content_scan(&cx, &mut tally, &buf, budget, findings, sink); + terminal = member_content_scan(&cx, &mut tally, &name, &buf, budget, findings, sink); } if let Some(name) = tally.encrypted.take() { if !matches!(terminal, Some(DeepOutcome::Infected { .. })) { @@ -2087,6 +2087,235 @@ where out } +/// The sets of **format-aware volumes** among a group of files that arrived +/// together, each put back into the one archive it was cut from and scanned: +/// RAR (`x.part1.rar`, `x.part2.rar`, ..., or `x.rar`, `x.r00`, ...), ZIP +/// (`x.z01`, `x.z02`, ..., `x.zip`) and CAB (`a.cab`, `b.cab`, as each header +/// names the one after it). Each volume carries its own headers, so the join is +/// the format's, not a concatenation, and the parts of a set scanned alone are +/// cut short. +/// +/// `fetch(name, None)` is a whole file and `fetch(name, Some(n))` its first `n` +/// bytes. Whole files are fetched only for the parts of a set, up to +/// `max-object-bytes` of them together; the heads of `.cab` files are read to +/// find which of them name a neighbour. Names are labels, never paths: a +/// cabinet's own header names its neighbours, and they are looked for in +/// `names` only. +/// +/// As [`analyze_volume_sets`]: one entry per *part*, with the verdict of the +/// archive that part belongs to, `Unscannable` for a set with a hole in it or +/// one that does not fit together. A file that is not part of a set is not in +/// the result. +#[cfg(feature = "all-formats")] +pub fn analyze_split_sets( + db: &Scanner, + names: &[String], + opts: &ScanOptions, + mut fetch: F, +) -> Vec +where + F: FnMut(&str, Option) -> io::Result>, +{ + use unpack::volume::{group, parse, Scheme}; + + #[derive(Clone, Copy, PartialEq)] + enum Kind { + Rar, + Zip, + Cab, + } + struct Set { + name: String, + kind: Kind, + parts: Vec, + /// Why the set cannot be joined, when it is known from the names. + broken: Option<&'static str>, + } + let mut sets: Vec = Vec::new(); + let lower = |s: &str| s.to_ascii_lowercase(); + + // RAR: the naming schemes say the order, and a gap says a part is missing. + let rar: Vec<&str> = names + .iter() + .map(String::as_str) + .filter(|n| { + matches!( + parse(n).map(|v| v.scheme), + Some(Scheme::RarPart { .. } | Scheme::RarOld) + ) + }) + .collect(); + for set in group(&rar) { + let parts: Vec<&str> = set.iter().map(|&i| rar[i]).collect(); + let first = parse(parts[0]).expect("grouped by name"); + let contiguous = parts + .iter() + .enumerate() + .all(|(i, p)| parse(p).is_some_and(|v| v.index == i)); + sets.push(Set { + name: format!("{}.rar", first.stem), + kind: Kind::Rar, + parts: parts.iter().map(|p| (*p).to_string()).collect(), + broken: (!contiguous).then_some("part of a multi-volume RAR set with a volume missing"), + }); + } + + // ZIP: `x.z01` ... and the `x.zip` that comes last and holds the directory. + let mut stems: Vec = Vec::new(); + for n in names { + if let Some(v) = parse(n).filter(|v| v.scheme == Scheme::ZipSplit) { + if !stems.iter().any(|s| s.eq_ignore_ascii_case(&v.stem)) { + stems.push(v.stem); + } + } + } + for stem in stems { + let mut numbered: Vec<(usize, &String)> = names + .iter() + .filter_map(|n| { + parse(n) + .filter(|v| v.scheme == Scheme::ZipSplit && v.stem.eq_ignore_ascii_case(&stem)) + .map(|v| (v.index, n)) + }) + .collect(); + numbered.sort(); + let last = names + .iter() + .find(|n| lower(n) == lower(&format!("{stem}.zip"))); + let contiguous = numbered.iter().enumerate().all(|(i, (idx, _))| *idx == i); + let mut parts: Vec = numbered.iter().map(|(_, n)| (*n).clone()).collect(); + let broken = match last { + Some(l) => { + parts.push(l.clone()); + (!contiguous).then_some("part of a multi-volume ZIP set with a volume missing") + } + None => Some( + "part of a multi-volume ZIP set whose last volume, the `.zip`, was not present", + ), + }; + sets.push(Set { + name: format!("{stem}.zip"), + kind: Kind::Zip, + parts, + broken, + }); + } + + // CAB: no naming scheme; each header names its neighbours. + let mut cabs: Vec<(String, unpack::CabLinks)> = Vec::new(); + for n in names.iter().filter(|n| lower(n).ends_with(".cab")) { + // The header, its reserved area and the two names fit in 64 KiB. + if let Some(l) = fetch(n, Some(64 * 1024)) + .ok() + .and_then(|h| unpack::cab_links(&h)) + { + cabs.push((n.clone(), l)); + } + } + let mut ids: Vec = cabs.iter().map(|(_, l)| l.set_id).collect(); + ids.sort_unstable(); + ids.dedup(); + for id in ids { + let mut members: Vec<&(String, unpack::CabLinks)> = + cabs.iter().filter(|(_, l)| l.set_id == id).collect(); + members.sort_by_key(|(_, l)| l.index); + // The header's name is a label: its last component, whatever separator. + let leaf = |s: &str| lower(s.rsplit(['/', '\\']).next().unwrap_or(s)); + let chained = members.iter().enumerate().all(|(i, (name, l))| { + usize::from(l.index) == i + && (i == 0) == l.prev.is_none() + && (i + 1 == members.len()) == l.next.is_none() + && l.next + .as_deref() + .is_none_or(|n| members.get(i + 1).is_some_and(|m| leaf(n) == lower(&m.0))) + && l.prev + .as_deref() + .is_none_or(|p| i > 0 && leaf(p) == lower(&members[i - 1].0)) + && !name.is_empty() + }); + sets.push(Set { + name: members[0].0.clone(), + kind: Kind::Cab, + parts: members.iter().map(|(n, _)| n.clone()).collect(), + broken: (!chained).then_some("part of a multi-volume CAB set whose cabinets do not follow one another as their headers say"), + }); + } + + let mut out = Vec::new(); + let mut held_total = 0usize; + for set in sets { + let verdict = |report: ScanReport, out: &mut Vec| { + for part in &set.parts { + out.push(VolumeSetVerdict { + name: part.clone(), + set: set.name.clone(), + report: report.clone(), + }); + } + }; + if let Some(reason) = set.broken { + verdict( + ScanReport::unscannable(reason.to_string(), Vec::new()), + &mut out, + ); + continue; + } + let mut data: Vec> = Vec::new(); + let mut lost = None; + for part in &set.parts { + match fetch(part, None) { + Ok(d) => { + held_total = held_total.saturating_add(d.len()); + if held_total > opts.deep_analysis_max as usize { + lost = Some("part of a multi-volume set too large to hold for rejoining (--max-object-bytes)"); + break; + } + data.push(d); + } + Err(_) => { + lost = + Some("part of a multi-volume set one of whose volumes could not be read"); + break; + } + } + } + held_total = held_total.saturating_sub(data.iter().map(Vec::len).sum()); + if let Some(reason) = lost { + verdict( + ScanReport::unscannable(reason.to_string(), Vec::new()), + &mut out, + ); + continue; + } + let joined: Result = match set.kind { + Kind::Rar | Kind::Cab => { + let refs: Vec<&[u8]> = data.iter().map(Vec::as_slice).collect(); + let joined = if set.kind == Kind::Rar { + unpack::join_rar_volumes(&refs) + } else { + unpack::join_cab_volumes(&refs) + }; + joined.map(|j| analyze(db, &j, opts)) + } + Kind::Zip => { + let parts = data + .into_iter() + .map(|d| Box::new(d) as Box) + .collect(); + unpack::span::ZipSpan::new(parts).map(|s| analyze_source(db, &s, opts)) + } + }; + let report = joined.unwrap_or_else(|e| { + ScanReport::unscannable( + format!("part of a multi-volume set that could not be rejoined: {e}"), + Vec::new(), + ) + }); + verdict(report, &mut out); + } + out +} + /// The name a part's SET is reported under, matching what /// `unpack::volume::Collector` names a joined set. A name that does not parse as /// a volume is its own set of one. @@ -2790,6 +3019,54 @@ fn member_metadata_scan( None } +/// Smallest and largest member worth remembering as scanned clean: below the +/// first, scanning again costs about what the digest does; past the second, the +/// digest is another read of a very large object. +const CLEAN_MEMO_MIN: usize = 64 * 1024; +const CLEAN_MEMO_MAX: usize = 512 * 1024 * 1024; + +/// A digest of a member's bytes and of what a scan of it reads from its place: +/// the type of its container, the type its container forces on it, the types +/// above it, and the extension of its name. `None` for a member outside the +/// sizes worth remembering. +fn clean_memo_key( + cx: &MemberCtx<'_>, + name: &str, + core_type: Option, + data: &dyn ByteSource, +) -> Option<[u8; 32]> { + use sha2::Digest; + let len = data.len(); + if !(CLEAN_MEMO_MIN..=CLEAN_MEMO_MAX).contains(&len) { + return None; + } + let ext = name + .rsplit_once('.') + .map_or("", |(_, e)| e) + .to_ascii_lowercase(); + let mut h = sha2::Sha256::new(); + h.update( + format!( + "{:?}|{core_type:?}|{:?}|{ext}|{len}|", + cx.member_container, + engine::ancestry_snapshot() + ) + .as_bytes(), + ); + let mut at = 0; + while at < len { + let w = data.window(at, byte_source::CHUNK.min(len - at)); + if w.is_empty() { + return None; + } + h.update(&w[..]); + at += w.len(); + } + let mut key = [0u8; 32]; + key.copy_from_slice(&h.finalize()); + Some(key) +} + /// Scan one member's bytes, as an object one level below its container. /// /// Split from [`member_metadata_scan`] because a member of a multi-volume set @@ -2800,6 +3077,7 @@ fn member_metadata_scan( fn member_content_scan( cx: &MemberCtx<'_>, tally: &mut MemberTally, + name: &str, data: &dyn ByteSource, budget: &mut Budget, findings: &mut Vec, @@ -2825,11 +3103,30 @@ fn member_content_scan( container: cx.member_container, core_type, filename: None, + name: Some(name), core: true, carve: true, embedded: false, }; - match scan_object(cx.db, data, obj, cx.opts, budget, findings, sink) { + // The same bytes in the same place, already scanned clean at this depth or + // shallower, cannot give a different answer, and a document that holds + // copies of itself would otherwise be scanned once per copy. + let seen = clean_memo_key(cx, name, core_type, data); + if seen + .as_ref() + .is_some_and(|k| engine::clean_seen(k, obj.depth)) + { + return None; + } + let truncated_before = engine::scan_was_truncated(); + let outcome = scan_object(cx.db, data, obj, cx.opts, budget, findings, sink); + if let (DeepOutcome::Clean, Some(k)) = (&outcome, seen) { + // A scan that stopped short on the way has not seen all of it. + if truncated_before || !engine::scan_was_truncated() { + engine::clean_seen_insert(k, obj.depth); + } + } + match outcome { DeepOutcome::Clean => None, // Nested unscannable/encrypted members are remembered, not propagated as // a stop: keep scanning the rest of this container. @@ -2866,6 +3163,11 @@ struct Obj<'a> { /// The input's name, for YARA's filename externals. Nothing inside the /// input has one. filename: Option<&'a str>, + /// The name the object has where it sits (the input's, or a member's), for + /// what only a name says, like which text format a plain file is. Unlike + /// `filename`, a member has one; what is carved or decoded does not. + #[cfg_attr(not(feature = "visible-text"), allow(dead_code))] + name: Option<&'a str>, /// Whether to match the object's own bytes. Off for an archive carved out /// of an object whose own match already covered them. core: bool, @@ -2888,6 +3190,7 @@ impl<'a> Obj<'a> { container: None, core_type: None, filename: opts.filename.as_deref(), + name: opts.filename.as_deref(), core: true, carve: true, embedded: false, @@ -2901,6 +3204,7 @@ impl<'a> Obj<'a> { container, core_type: None, filename: None, + name: None, core: true, carve: true, embedded: false, @@ -3748,6 +4052,58 @@ fn normalizations(ft: FileType, data: &dyn ByteSource) -> Vec<(ViewKind, FileTyp v } +/// The text a reader sees in the object `data` called `name` (Markdown, HTML, +/// an Office or OpenDocument XML part, PDF, a drawing): its words, and where +/// each link leads (`exav_render::text`). +/// +/// The text as a text view is matched, and each link whose label is not its +/// target as `(label, target)`. +/// +/// `None` when the object has none, or when it cannot be made, and the scan is +/// then marked incomplete: the object is past `deep_analysis_max`, so it is +/// neither held in memory nor read whole. A text cut at four times the object and a little, +/// which no real document comes to, and a PDF past the extraction limits, mark +/// the scan incomplete too; what was made is still scanned. So does an +/// office document (with `visible-text-office`) whose text cannot be taken +/// out: encrypted, past a limit, damaged. +#[cfg(feature = "visible-text")] +fn visible_text_view(name: &str, data: &dyn ByteSource, opts: &ScanOptions) -> Option { + if !exav_render::text::wants(name, &data.window(0, 1024)) { + return None; + } + let bytes = whole(data, opts, "the reading of its visible text")?; + let cap = bytes + .len() + .saturating_mul(4) + .saturating_add(64 << 10) + .min(usize::try_from(opts.deep_analysis_max).unwrap_or(usize::MAX)); + let visible = exav_render::text::visible_text(name, &bytes, cap, &opts.limits)?; + if visible.truncated { + engine::mark_scan_truncated(); + } + // A document of a kind that has text, whose text could not be taken out + // (encrypted, past a limit, damaged): its bytes are still scanned, and the + // scan is not complete. + if visible.unreadable.is_some() { + engine::mark_scan_truncated(); + return None; + } + // As any text view is, so that `Target:7` signatures, written against the + // lowercased, whitespace-collapsed form, apply. + Some(VisibleView { + text: normalize::text(visible.text.as_bytes()), + links: visible.links, + }) +} + +/// A document's visible text as a text view, and its links. +#[cfg(feature = "visible-text")] +struct VisibleView { + text: Vec, + /// Each link whose label is not its target, as `(label, target)`. + links: Vec<(String, String)>, +} + /// A normalised view of an object: [`normalize`]'s three, and [`jsnorm`]'s. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ViewKind { @@ -3938,8 +4294,13 @@ fn core_matches( }; // A PE's structure (layout, icons, section table) is parsed from all of it. let image = pe_image(ft, data, opts); - // PE layout lets the engine resolve EP/section-relative offsets. - let layout = image.as_deref().and_then(pe::layout); + // PE layout lets the engine resolve EP/section-relative offsets, and so + // does an ELF's or a Mach-O's, read from its headers. + let layout = image.as_deref().and_then(pe::layout).or_else(|| match ft { + FileType::Elf => pe::elf_layout(data), + FileType::MachO => pe::macho_layout(data), + _ => None, + }); // PE-icon perceptual metrics for `IconGroup1/2`-constrained logical // signatures: computed only for a PE when `.idb` entries are loaded. They // gate such sigs (the structural condition must also hold); see [`icon`]. @@ -4001,6 +4362,30 @@ fn core_matches( engine_pass(view.source(), as_type, None, None, None)?; } } + // The words of a document, and where each link leads: what a reader sees, + // which the bytes above do not give (a label naming one site over a target + // on another, markup, an entity, a compressed stream or an escape in the + // middle of a signature's text). + #[cfg(feature = "visible-text")] + if !opts.clamav_compat && !db.engine.is_empty() { + if let Some(view) = profile::timed("visible-text", data.len() as u64, || { + visible_text_view(obj.name.unwrap_or(""), data, opts) + }) { + // Reported where the document is, as the other views are. + engine_pass(&view.text, FileType::TextView, None, None, None)?; + // A link that says one site and goes to another, in a document + // that is not HTML: judged as an anchor of an HTML one is. + if opts.alert_phishing { + let (found, complete) = phishing::scan_links(&view.links, &db.phishing); + if !complete { + engine::mark_scan_truncated(); + } + if let Some(p) = found { + hit(p.signature(), false, 0, Method::Heuristic)?; + } + } + } + } // Bytecode programs whose trigger/hook fires (gated execution). Their // names are reported verbatim, never `.UNOFFICIAL`-suffixed. let mut unpacked = Vec::new(); @@ -5123,6 +5508,111 @@ mod tests { } } + /// 128 KiB of text that differs with `seed`. + #[cfg(feature = "all-formats")] + fn memo_body(seed: usize) -> Vec { + let text: String = (0..4096) + .map(|i| format!("line {seed} {i} of a member with nothing in it\n")) + .collect(); + text.as_bytes()[..128 * 1024].to_vec() + } + + /// A stored zip of `n` members of `memo_body`, the same in each when `same`. + #[cfg(feature = "all-formats")] + fn memo_zip(n: usize, same: bool) -> Vec { + use std::io::Write; + let stored = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + for k in 0..n { + z.start_file(format!("member{k}.txt"), stored).unwrap(); + z.write_all(&memo_body(if same { 0 } else { k })).unwrap(); + } + z.finish().unwrap().into_inner() + } + + fn clean_skips_of(data: &[u8]) -> (Verdict, u64) { + engine::CLEAN_SKIPS.with(|c| c.set(0)); + let v = analyze(&Scanner::builtin(), data, &ScanOptions::default()).verdict; + (v, engine::CLEAN_SKIPS.with(|c| c.get())) + } + + /// Bytes already scanned clean in the same place are not scanned again: + /// the first of 40 copies is scanned and the other 39 are not. + #[test] + #[cfg(feature = "all-formats")] + fn copies_of_a_clean_member_are_scanned_once() { + assert_eq!(clean_skips_of(&memo_zip(40, true)), (Verdict::Clean, 39)); + } + + /// The control: members that differ are each scanned. + #[test] + #[cfg(feature = "all-formats")] + fn members_that_differ_are_each_scanned() { + assert_eq!(clean_skips_of(&memo_zip(40, false)), (Verdict::Clean, 0)); + } + + /// Members under the size worth remembering are scanned every time. + #[test] + #[cfg(feature = "all-formats")] + fn small_copies_are_each_scanned() { + use std::io::Write; + let stored = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + for k in 0..10 { + z.start_file(format!("m{k}.txt"), stored).unwrap(); + z.write_all(&memo_body(0)[..1000]).unwrap(); + } + let z = z.finish().unwrap().into_inner(); + assert_eq!(clean_skips_of(&z), (Verdict::Clean, 0)); + } + + /// A copy that is not clean is found wherever it is, after clean ones with + /// the same start. + #[test] + #[cfg(feature = "all-formats")] + fn an_infected_member_after_clean_copies_is_found() { + use std::io::Write; + let stored = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + // Deflated, so the signature is in the member only, not in the zip's bytes. + let deflated = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated); + let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + for k in 0..3 { + z.start_file(format!("clean{k}.bin"), stored).unwrap(); + z.write_all(&memo_body(0)).unwrap(); + } + let mut dirty = memo_body(0); + dirty.extend_from_slice(&[eicar(), b"\n"].concat().repeat(64)); + z.start_file("dirty.bin", deflated).unwrap(); + z.write_all(&dirty).unwrap(); + let z = z.finish().unwrap().into_inner(); + let (v, skips) = clean_skips_of(&z); + assert_eq!(skips, 2); + assert!(matches!(v, Verdict::Infected { .. }), "{v:?}"); + } + + /// The memo remembers the shallowest depth a clean scan was at: a copy + /// deeper than that is covered, one shallower is not. + #[test] + fn a_clean_copy_covers_only_the_same_depth_or_deeper() { + engine::reset_scan_truncated(); + let key = [7u8; 32]; + assert!(!engine::clean_seen(&key, 3)); + engine::clean_seen_insert(key, 3); + assert!(!engine::clean_seen(&key, 2)); + assert!(engine::clean_seen(&key, 3)); + assert!(engine::clean_seen(&key, 9)); + engine::clean_seen_insert(key, 1); + assert!(engine::clean_seen(&key, 1)); + assert!(!engine::clean_seen(&key, 0)); + // A new scan starts with nothing remembered. + engine::reset_scan_truncated(); + assert!(!engine::clean_seen(&key, 9)); + } + #[test] fn detects_eicar_pattern() { let db = Scanner::builtin(); diff --git a/crates/exav-core/src/pe.rs b/crates/exav-core/src/pe.rs index a8723f2..6634894 100644 --- a/crates/exav-core/src/pe.rs +++ b/crates/exav-core/src/pe.rs @@ -538,6 +538,160 @@ pub fn layout(data: &[u8]) -> Option { }) } +/// Most sections an ELF or Mach-O layout lists: a section table is a `u16` +/// long in an ELF, and nothing a real file has comes near it. +const MAX_EXEC_SECTIONS: u64 = 1 << 16; + +/// [`PeLayout`] of an ELF file, for the `EP`, `Sn`, `SL` and `SEn` offsets of a +/// `Target:6` signature, read from the headers alone (the object is not held). +/// +/// As clamscan resolves them (measured, with one signature per offset and one +/// wrong by three bytes as the control): `EP` is the entry point's file +/// offset, found by the loadable segment whose memory image holds it (the +/// part past the file's bytes, the `.bss`, included), and `Sn` is the file +/// offset of section header `n`, counted from the null section at 0. +/// A section with no bytes in the file (`SHT_NOBITS`) is a point. +pub fn elf_layout(src: &dyn ByteSource) -> Option { + let mut f = Fields::new(src); + let ident = f.get(0, 16)?; + if &ident[..4] != b"\x7fELF" { + return None; + } + let wide = match ident[4] { + 1 => false, + 2 => true, + _ => return None, + }; + // Anything but little-endian is read as big-endian, as `elf_broken` does. + f.be = ident[5] != 1; + let (header, ph_min, sh_min) = if wide { (64, 56, 64) } else { (52, 32, 40) }; + f.get(0, header)?; + let w = if wide { 8 } else { 4 }; + let entry_va = f.word(24, wide)?; + let (phoff, shoff) = (f.word(24 + w, wide)?, f.word(24 + 2 * w, wide)?); + let fields = 28 + 3 * w; + let [ph_entsize, phnum, sh_entsize, shnum] = + [2, 4, 6, 8].map(|i| f.u16(fields + i).map(u64::from)); + let (ph_entsize, phnum, sh_entsize, shnum) = + (ph_entsize?, phnum?, sh_entsize?, shnum?); + + let mut entry = None; + if ph_entsize >= ph_min { + let (offset_at, vaddr_at, memsz_at) = if wide { (8, 16, 40) } else { (4, 8, 20) }; + for i in 0..phnum { + let p = phoff.checked_add(i * ph_entsize)?; + let (Some(kind), Some(offset), Some(vaddr), Some(memsz)) = ( + f.u32(p), + f.word(p + offset_at, wide), + f.word(p + vaddr_at, wide), + f.word(p + memsz_at, wide), + ) else { + break; + }; + // PT_LOAD + if kind == 1 && entry_va >= vaddr && entry_va - vaddr < memsz { + entry = offset.checked_add(entry_va - vaddr); + break; + } + } + } + + let mut section_rawptrs = Vec::new(); + let mut section_rawsizes = Vec::new(); + if sh_entsize >= sh_min { + let (offset_at, size_at) = if wide { (24, 32) } else { (16, 20) }; + for i in 0..shnum.min(MAX_EXEC_SECTIONS) { + let Some(s) = shoff.checked_add(i * sh_entsize) else { + break; + }; + let (Some(kind), Some(offset), Some(size)) = + (f.u32(s + 4), f.word(s + offset_at, wide), f.word(s + size_at, wide)) + else { + break; + }; + section_rawptrs.push(offset); + // SHT_NOBITS + section_rawsizes.push(if kind == 8 { 0 } else { size }); + } + } + Some(PeLayout { + entry, + section_rawptrs, + section_rawsizes, + version_info: Vec::new(), + }) +} + +/// [`PeLayout`] of a thin Mach-O file, for the `Sn`, `SL` and `SEn` offsets +/// of a `Target:9` signature. `n` counts the sections of every segment in the +/// order of the load commands, and an offset is the section's own. +/// +/// `EP` is left unresolved: clamscan 1.4.3 does not resolve it on a Mach-O of +/// either width, from `LC_MAIN` or from `LC_UNIXTHREAD` (a signature +/// anchored there never fired in any of eight variants, where the section +/// offsets did, each with a control), so a signature using it matches +/// nowhere, as it does there. +pub fn macho_layout(src: &dyn ByteSource) -> Option { + let mut f = Fields::new(src); + let (wide, be) = match f.get(0, 4)? { + [0xCE, 0xFA, 0xED, 0xFE] => (false, false), + [0xCF, 0xFA, 0xED, 0xFE] => (true, false), + [0xFE, 0xED, 0xFA, 0xCE] => (false, true), + [0xFE, 0xED, 0xFA, 0xCF] => (true, true), + _ => return None, + }; + f.be = be; + let header: u64 = if wide { 32 } else { 28 }; + let (ncmds, sizeofcmds) = (u64::from(f.u32(16)?), u64::from(f.u32(20)?)); + let end = header.checked_add(sizeofcmds)?; + if end > src.len() as u64 { + return None; + } + // LC_SEGMENT and LC_SEGMENT_64, and what each one's sections are like. + let (segment, nsects_at, first_section, section_len, size_at, offset_at) = if wide { + (0x19u32, 64u64, 72u64, 80u64, 40u64, 48u64) + } else { + (0x1, 48, 56, 68, 36, 40) + }; + let mut section_rawptrs = Vec::new(); + let mut section_rawsizes = Vec::new(); + let mut at = header; + 'commands: for _ in 0..ncmds.min(MAX_EXEC_SECTIONS) { + let (Some(cmd), Some(size)) = (f.u32(at), f.u32(at + 4).map(u64::from)) else { + break; + }; + if size < 8 || at.checked_add(size)? > end { + break; + } + if cmd == segment { + let Some(nsects) = f.u32(at + nsects_at).map(u64::from) else { + break; + }; + for i in 0..nsects { + let s = at + first_section + i * section_len; + if s + section_len > at + size || section_rawptrs.len() as u64 >= MAX_EXEC_SECTIONS { + break 'commands; + } + let (Some(offset), Some(len)) = + (f.u32(s + offset_at).map(u64::from), f.word(s + size_at, wide)) + else { + break 'commands; + }; + section_rawptrs.push(offset); + // A zero-fill section has no bytes in the file. + section_rawsizes.push(if offset == 0 { 0 } else { len }); + } + } + at += size; + } + Some(PeLayout { + entry: None, + section_rawptrs, + section_rawsizes, + version_info: Vec::new(), + }) +} + /// One PE section in the shape the bytecode PE APIs expect /// (`cli_exe_section`). /// @@ -778,9 +932,34 @@ pub(crate) fn embedded_in(data: &dyn ByteSource) -> Embedded { pub(crate) struct Carving { finders: Vec>, hits: Vec<(usize, Magic)>, + /// Where the last ZIP local header seen says the next entry begins, when + /// it can say (see [`zip_next_header`]). + zip_next: Option, pub(crate) found: Embedded, } +/// Where the ZIP local header at `off` says the next entry begins: after its +/// name, extra field and compressed data. `None` when it cannot say: the entry +/// is encrypted, its sizes follow the data (a data descriptor), or its size is +/// ZIP64's. +fn zip_next_header(data: &dyn ByteSource, off: usize) -> Option { + let h = data.window(off, 30); + if h.len() != 30 || h[..4] != *b"PK\x03\x04" { + return None; + } + let flags = u16::from_le_bytes([h[6], h[7]]); + if flags & 0b1001 != 0 { + return None; + } + let csize = u32::from_le_bytes([h[18], h[19], h[20], h[21]]); + if csize == u32::MAX { + return None; + } + let name = usize::from(u16::from_le_bytes([h[26], h[27]])); + let extra = usize::from(u16::from_le_bytes([h[28], h[29]])); + off.checked_add(30 + name + extra)?.checked_add(csize as usize) +} + impl Carving { /// Bytes each window must run into the next, so a magic across the seam /// is seen whole. @@ -800,6 +979,7 @@ impl Carving { Carving { finders: CARVE_MAGICS.iter().map(|(m, _)| memchr::memmem::Finder::new(m)).collect(), hits: Vec::new(), + zip_next: None, found: Embedded::default(), } } @@ -822,14 +1002,30 @@ impl Carving { pub(crate) fn feed(&mut self, data: &dyn ByteSource, at: usize, w: &[u8], owned: usize) { let found = &mut self.found; let hits = &mut self.hits; + let zip_next = &mut self.zip_next; hits.clear(); for (f, (_, kind)) in self.finders.iter().zip(CARVE_MAGICS) { hits.extend(f.find_iter(w).take_while(|&p| p < owned).map(|p| (at + p, kind))); } hits.sort_unstable_by_key(|&(off, _)| off); for &(off, kind) in hits.iter() { + let zip = matches!(kind, Magic::Archive) && data.window(off, 4)[..] == *b"PK\x03\x04"; if off == 0 { - continue; // the object's own image or archive + // The object's own image or archive; a ZIP's later entries + // are its own, not archives stapled inside it. + if zip { + *zip_next = zip_next_header(data, off); + } + continue; + } + // The next entry of a ZIP already taken is read when that ZIP is: + // its walk reaches every local header after its own, wherever the + // directory points. Counting each as an archive of its own would + // fill the cap with one zip's members and rescan the rest of the + // object once per member. + if zip && *zip_next == Some(off) { + *zip_next = zip_next_header(data, off); + continue; } let (list, cap, ok) = match kind { Magic::Pe => (&mut found.pe, MAX_EMBEDDED_PE, pe_at(data, off)), @@ -838,9 +1034,16 @@ impl Carving { Magic::MachoFat => (&mut found.macho, MAX_EMBEDDED_PE, macho_fat_at(data, off)), Magic::Archive => (&mut found.archives, MAX_EMBEDDED_ARCHIVES, archive_at(data, off)), }; + // The chain follows only a ZIP that will be scanned. + if zip { + *zip_next = None; + } if ok { if list.len() < cap { list.push(off); + if zip { + *zip_next = zip_next_header(data, off); + } } else if matches!(kind, Magic::Pe) { found.pe_capped = true; } else { @@ -1235,6 +1438,82 @@ mod tests { assert!(found.others_capped); } + /// A ZIP local header for an entry named `name` holding `len` bytes, with + /// the general-purpose `flags` and a declared compressed size of `csize`, + /// followed by `len` bytes of data. + fn zip_entry(name: &str, flags: u16, csize: u32, len: usize) -> Vec { + let mut e = b"PK\x03\x04\x14\0".to_vec(); + e.extend_from_slice(&flags.to_le_bytes()); + e.extend_from_slice(&[0, 0, 0, 0, 0x21, 0, 0, 0, 0, 0]); + e.extend_from_slice(&csize.to_le_bytes()); + e.extend_from_slice(&(len as u32).to_le_bytes()); + e.extend_from_slice(&(name.len() as u16).to_le_bytes()); + e.extend_from_slice(&[0, 0]); + e.extend_from_slice(name.as_bytes()); + e.extend_from_slice(&vec![b'.'; len]); + e + } + + /// `entries` entries of a ZIP (each declaring its size truthfully) after a + /// text prefix. + fn zip_after_prefix(entries: usize, flags: u16) -> (Vec, usize) { + let mut data = b"a carrier with a zip stapled inside it\n".to_vec(); + let first = data.len(); + for k in 0..entries { + data.extend(zip_entry(&format!("m{k}"), flags, 20, 20)); + } + (data, first) + } + + /// The entries of one ZIP stapled into an object are read when the ZIP is, + /// so only the first is a candidate: they neither fill the cap nor are the + /// object rescanned once per entry. + #[test] + fn the_entries_of_a_zip_are_not_archives_of_their_own() { + let (data, first) = zip_after_prefix(MAX_EMBEDDED_ARCHIVES * 2, 0); + let found = embedded_in(&&data[..]); + assert_eq!(found.archives, [first]); + assert!(!found.others_capped); + } + + /// A ZIP that is the object itself: every header after its own is an entry. + #[test] + fn the_entries_of_a_zip_at_offset_zero_are_not_archives_of_their_own() { + let (data, first) = zip_after_prefix(40, 0); + let found = embedded_in(&&data[first..]); + assert!(found.archives.is_empty() && !found.others_capped); + } + + /// Where the header does not say where the next entry begins, each header + /// stays a candidate: a data descriptor, encryption or ZIP64's size leaves + /// the position unknown. + #[test] + fn a_header_that_cannot_place_the_next_one_keeps_every_candidate() { + for flags in [0b1000, 0b0001] { + let (data, _) = zip_after_prefix(5, flags); + assert_eq!(embedded_in(&&data[..]).archives.len(), 5, "flags {flags:#b}"); + } + let mut data = b"carrier\n".to_vec(); + for k in 0..5 { + data.extend(zip_entry(&format!("m{k}"), 0, u32::MAX, 20)); + } + assert_eq!(embedded_in(&&data[..]).archives.len(), 5, "ZIP64 size"); + } + + /// A header that names a size that does not lead to the next header does + /// not hide it: a second archive placed anywhere else is still a + /// candidate. + #[test] + fn a_size_that_misses_the_next_header_keeps_it_a_candidate() { + let mut data = b"carrier\n".to_vec(); + let first = data.len(); + // Declares 10 bytes of data, holds 20: the chain lands inside them. + data.extend(zip_entry("a", 0, 10, 20)); + let second = data.len(); + data.extend(zip_entry("b", 0, 20, 20)); + assert_eq!(embedded_in(&&data[..]).archives, [first, second]); + } + #[test] fn embedded_macho_carved_and_java_rejected() { // A 64-bit LE Mach-O executable header (MH_MAGIC_64 = CF FA ED FE) at a diff --git a/crates/exav-core/src/phishing.rs b/crates/exav-core/src/phishing.rs index 6a01295..ace8d4a 100644 --- a/crates/exav-core/src/phishing.rs +++ b/crates/exav-core/src/phishing.rs @@ -235,6 +235,30 @@ pub(crate) fn scan_complete_source( (None, true) } +/// [`scan_complete`] over links already read: `(display, href)` pairs, from a +/// document that is not HTML (Markdown, a PDF) and has its own way of saying +/// where a link goes. Judged as an anchor is, allow-list included. +#[cfg(feature = "visible-text")] +pub(crate) fn scan_links(links: &[(String, String)], db: &PhishingDb) -> (Option, bool) { + let mut allow_checks = 0usize; + for (display, href) in links { + let Some(p) = classify(href, display, db) else { + continue; + }; + if db.has_allow_list() { + if allow_checks == MAX_ALLOW_CHECKS { + return (None, false); + } + allow_checks += 1; + if allow_listed(href, display, db) { + continue; + } + } + return (Some(p), true); + } + (None, true) +} + /// Offset of the next ` Result<&mut Self> { + check_paren_depth(src)?; let ast = AST::from(src); if let Some(err) = ast.errors().first() { return Err(Error::new(format!("parse error: {err:?}"))); @@ -190,6 +191,10 @@ impl Compiler { /// the returned list. pub fn add_source_lenient(&mut self, src: &str) -> Vec<(String, Error)> { let mut rejected = Vec::new(); + if let Err(e) = check_paren_depth(src) { + rejected.push((String::new(), e)); + return rejected; + } let ast = AST::from(src); if let Some(err) = ast.errors().first() { rejected.push((String::new(), Error::new(format!("parse error: {err:?}")))); @@ -1419,6 +1424,72 @@ fn const_to_cond(v: crate::yara::ir::Value) -> Cond { /// Maps an integer file-read function name to its [`IntKind`], or `None` for /// non-integer reads (e.g. `float32`, which is not implemented). Single-byte /// `_be` variants are equivalent to their little-endian forms. +/// Deepest parenthesis nesting a rule source may have. The parser tries each +/// `(` twice, as a boolean group and as an arithmetic one, and does not keep +/// what the first try parsed, so a rule with an error inside `d` open +/// parentheses costs about 2^d steps (a 3.5 KB rule with 15 took 5.6 s). +const MAX_PAREN_DEPTH: usize = 12; + +/// Refuses a source nested deeper than [`MAX_PAREN_DEPTH`] before it is parsed. +/// Strings, comments and regular expressions are skipped. +fn check_paren_depth(src: &str) -> Result<()> { + let b = src.as_bytes(); + let (mut i, mut depth) = (0, 0usize); + // The last byte that was not whitespace, to tell a regex from a division. + let mut prev = 0u8; + while i < b.len() { + let c = b[i]; + match c { + b'"' => { + i += 1; + while i < b.len() && b[i] != b'"' { + i += if b[i] == b'\\' { 2 } else { 1 }; + } + } + b'/' if b.get(i + 1) == Some(&b'/') => { + while i < b.len() && b[i] != b'\n' { + i += 1; + } + } + b'/' if b.get(i + 1) == Some(&b'*') => { + i += 2; + while i < b.len() && !b[i..].starts_with(b"*/") { + i += 1; + } + i += 1; + } + b'/' if prev == b'=' || b[..i].trim_ascii_end().ends_with(b"matches") => { + i += 1; + let mut class = false; + while i < b.len() && (class || b[i] != b'/') { + match b[i] { + b'\\' => i += 1, + b'[' => class = true, + b']' => class = false, + _ => {} + } + i += 1; + } + } + b'(' => { + depth += 1; + if depth > MAX_PAREN_DEPTH { + return Err(Error::new(format!( + "parentheses nested more than {MAX_PAREN_DEPTH} deep" + ))); + } + } + b')' => depth = depth.saturating_sub(1), + _ => {} + } + if !c.is_ascii_whitespace() { + prev = c; + } + i += 1; + } + Ok(()) +} + fn int_kind(name: &str) -> Option { Some(match name { "uint8" | "uint8be" => IntKind::U8, @@ -1434,3 +1505,59 @@ fn int_kind(name: &str) -> Option { _ => return None, }) } + +#[cfg(test)] +mod paren_depth_tests { + use super::{Compiler, MAX_PAREN_DEPTH}; + use std::time::{Duration, Instant}; + + fn nested(depth: usize) -> String { + format!( + "rule r {{ condition: {}true{} }}", + "(".repeat(depth), + ")".repeat(depth) + ) + } + + /// A 3.5 KB rule with an error inside 15 open parentheses (`yara`, fuzz + /// timeout): the parser took 5.6 s to refuse it. + #[test] + fn a_rule_with_an_error_deep_in_parentheses_is_refused_fast() { + let src = include_str!("../../tests/fixtures/fuzz/yara_nested_parens.yar"); + let start = Instant::now(); + let err = Compiler::new().add_source(src).err().expect("refused"); + assert!( + start.elapsed() < Duration::from_millis(500), + "{:?}", + start.elapsed() + ); + assert!(err.to_string().contains("nested"), "{err}"); + let start = Instant::now(); + let rejected = Compiler::new().add_source_lenient(src); + assert!( + start.elapsed() < Duration::from_millis(500), + "{:?}", + start.elapsed() + ); + assert_eq!(rejected.len(), 1); + } + + #[test] + fn nesting_up_to_the_bound_compiles_and_one_more_is_refused() { + assert!(Compiler::new().add_source(&nested(MAX_PAREN_DEPTH)).is_ok()); + assert!(Compiler::new() + .add_source(&nested(MAX_PAREN_DEPTH + 1)) + .is_err()); + } + + #[test] + fn parentheses_in_strings_comments_and_regexes_are_not_counted() { + let many = "(".repeat(40); + let src = format!( + "rule r {{ strings: $a = \"{many}\" $b = /x\\({many_esc}y/ $c = /[{many}]/ \ + condition: // {many}\n /* {many} */ ($a or $b or $c) and \"{many}\" contains \"(\" }}", + many_esc = "\\(".repeat(40) + ); + Compiler::new().add_source(&src).expect("compiles"); + } +} diff --git a/crates/exav-core/tests/fixtures/fuzz/yara_nested_parens.yar b/crates/exav-core/tests/fixtures/fuzz/yara_nested_parens.yar new file mode 100644 index 0000000000000000000000000000000000000000..68cb702fa57026d703dc36d557aebaaae19f4822 GIT binary patch literal 3521 zcmeHKUvJYe5Fal*L_$0u@qC0lq!aCuwp3lk{&mV;wmvW>fmBt)N!-;Uf2!Dpt)qMp zz8YVKvzw%;+LcZdNJX>qV8{0P?swMDp`5`Yx54>rV!5+H@JNYtukvjng9z`_X_;UBwU=1P>IT7o^Cye6*a z7q`yVpv==YkjO9UZg<)xDbS~V7Exab-j#b&&9-c-q~*4a#^cM6Ebvp?xJa0^?77rY zHiMvrw@|a;*n)1g-0XqrG~qiqyCpa{n0-Gi3lW=cf)|FXwl&*OugW!Dt6c9vvnKl{ zST+hk;0DAKG+|sYkHbD>4xIW-p)#Zu!9vOf35U4CnpfNj`l;SCH)bjThCxN#45G9muztAM2L9JTxgFt@+`dLi;Gv+2FxjE7mwz!&J zW3;U+JBmHRso-WG_mCifxDTd5woA~0>^y+FPvEUYs0bw(9zwPAEl#?mJns2B%D^S| zlIRA_lfj_O{eyYGepV~%dQjaK8mpp$m)ZI1ejM*Dj_fGWp80&b-Pe* LscKBtT*LS`l|0w? literal 0 HcmV?d00001 diff --git a/crates/exav-core/tests/fixtures/volumes/set4.r00 b/crates/exav-core/tests/fixtures/volumes/set4.r00 new file mode 100644 index 0000000000000000000000000000000000000000..3cd62dcc6553b6ca89211a312e314c943f7a4ce3 GIT binary patch literal 8192 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 literal 0 HcmV?d00001 diff --git a/crates/exav-core/tests/fixtures/volumes/set5.part2.rar b/crates/exav-core/tests/fixtures/volumes/set5.part2.rar new file mode 100644 index 0000000000000000000000000000000000000000..344e4d8c8c0f81632cf022f42da3f9032e6bfebc GIT binary patch literal 8192 zcmb7}V^<{%!-cb5lWp5JPBtgIrju*3&B;x+b+Vi6$vhd8Z9Vsw_XoUt?GM*K*y~zb z!_1X}5FQdj_9{pY1rin>8WIi?GPf@XLi2#|o&kyod8-!!rXUBhITT7l%JO#*G$f)E z(9X?@)!fbr2^!l|uGc=oOrI>$>&{6y?30^yV6J#@T!WPTUaOP-j-!M207cFp8Mw)_ z@Q&E;{tX9HnAx;)Qpo>|jeNtfIA5Vb_gcMMKQr+lHi79jb+whuXB;{(`$Bb-hLK#bjkU;tLr+~P! zo%MSD1q}cfj6!qhET%QXlcW$0`g1BC4$mZ=Wr?RJ5XpsOW1Cc<(U?A4lqRvc>Y z9Iydo`f5NKtvIftFEU2AW92Kg3oyPWgMr zH1gaGyK@zBC-DeEbD@Fn5HhiTHvxGmQe)pr&)Bw|&TMkY zX9ZPuR^R4dlX#zC;ky<%;qz@ww`EYc%L#k|kve?rNpDpII+3rf5>0|RTGi-Xa$aUz zXNg1Kx>}SD8xl1Kp!O4Qkp3h2Ugp(OWQfP8<}@m|4m0OZ9osi((HCOtF$Mh!DD;ti zm(qKmR9lp!=9XMIC>fpSB8E3w$WJ$utXvG$n515;yuaQY!1^B&pagTfG5ecL%)=SK zxOF+S3h2oFnl@H=<;-*QHX=EqZI!xvxYLEb`;KiY)qhzw5%-L-j-D>3xToMs#(??9 zUhHRcR?^hVw~6bh-BCo&<49hC&>k9_n}=9SDI}#@oz82!2RLSrh4ROgFj!&n?26mx z7qiR<=B+0e|7#^4aH_x-`s+~s@$Dz(43EJQqK}xh`$dW7-K4@jN(YO{BtTL<96VQ* zujkFnom}_@bC7Y4A;S2Wd$eQjGqAmnYFaIhdV2V&sT?(i`K}`cMhZqqRtmb~i*1}; zvX>yTzO=&|_AZ(c5{>Qv`o?*CoXNZiNYi;q92y~u)AlXG)~cj#0V$s@4GmDs=ikk5 zMSFK!u(~`cQKc?cD3NHki5)R~%M=ra_8wie`6wwaPw`X6p-(iV@3G%H;l^-TXaz?f zco#k8Y*L5@b|8MSI2`-8>cni=o3*;|p_A$}%t?W~);edH1@IwiQQUYuEa0(zYIb?W ze&X}foXaCSu_3rT%UOxW68JBq8w%86Al=ancgiXDAmKuDmA;%jJbj$+!6bw*duVTj z@;O8`(KY=o$($=?HrKn-_02tmCwRws!N713wg#I4^yRxw^osES7j@k z3KNje7-7ILYB-B)o2^vBj7Y675nGvZD}QqVg`K=2o%0PzB>UYPZEmbt^+t;9g;4Mb ziDO}Qm)r{L#5x_6lqX?Zc#SL+UC?3T>~=UCIFc;bLyC+^TYpN0#@(A~&^+>?Vlug( z=etVa0QA1oh+yAG&7Spt1%PI-W}#g~`?BW?FoNs!lVP);UKFh?@oYUT$%_RWQlQc4^;U4MS4hcLq@eL6}Y zB>;b=+dr15i2B7o{&53qO;MENrMK3R_eB2E0B)N~rcbr4GeE6Fxg6}CcP%bD z`Y)8Ahn??#nh0)Vvc0{i#RlX6O*$9Pn`hIY8RiKWwU&vb-~v8M7`!Px=EcXWjx{&N zK8rqJ4|sI$?regCmHY1rJ2W#F)&806pF}0v8o4t-#&T~zy>CRqMECiKY}Z`bfWQ4>FH; z)d>I4H!yUD)5$P2P4P);)%1sT@~_sSx*E<_8n>k4X&}mnb&wYyRU+0cp5Ho zEZ5ZP9h6c zgxWOxEK5dmo6ps2w&jzCR%4sg59FSfqtRA+4s@$;JHci)N`Kut+ZQhT7(+Mx#pvQe zJe2`Zn{kAjW>0UVb?zzQKtM>Jg~FQV+qu3iep3z!!g3@=xBh_#LSodJ)$NB^D7)5zo4Av7=bK9LjSV<}t??bK!qo1vel)#U1o3)I5sZhkSqtdz&{5k-49`$>%NfcD5gRQzP)@e65)!Yv8;|c*_%c}2LLPBf zU3+SGBYg0DDXs<+#isw-L;6+gP-_JJ=U1)KE&X`FH-6np-Ik7}N)nTLPiO3v!(^*8 zEDy*-7*k#`yiYQsql3)sp=?IMYP6SoN4Vj%mOqPxov#Am>S z38Q-SDSCS_nGQWC{VC^RG5@IkN{PehKT-HVML5ALmJ>}!aW4IF8H{AcH{zHx=?KRR zzxE|%4jj6nKY+RzWIlLy7F|)7nqf{8zE~Z#(_|s^dj?88ebiFb*hZo=?ZJKEM72umhM#DIXgtH`751KTNmX}P81PFi?3vbuO0$0%jK z^tr5N74Fnclw2$!6z!e(Zu35m%`;hv4G`Bx9KgoWcZx|8XEa;DOyqmWA2j?D9 z>{(qQn;<4o5FM^%Cpyr{@kZHO$pqqBRL&zNqoT8GT%o;o zYUbn6@gu`5n`$H$CVH&7)+dR4y2ulA6+CS+SCHj0?Qxo$?5(ljRDY)YqNSmX&wk}s z5@UD4%sIo!hb;0J4Dk2c_!lgYvw~Fb*3hup8rNa`ldDVyvCgu?1hD>-DKvkVzBJ5S z{+Lupr64uPMC5^L{8KYtS<@_On!mUfhIr6rw7V*AB=%?>7TTmg^IoS~_V*H6!meR2 zC?!@z_bl(?2W+~Y>**(x?O{P_Z2Uh7PR9>1oAglF^joBR>9(WlT5%|!=-xOMnpczJcWK%4L@W689kIcgQZvbo)aW??U@oen zV6Tf6r3AiRkt-v#lRKK#HSx1BR_H->YEhEVVz|~sYTN|IRcDDsOCa}c`P#Y9eF*sQ z`r-XrVaPnync4OqoudZA`2gOafga6RFO=G0(%DG!Gd;e26U07I?E^{19n2`Z$ zKs~Ak6wjga7R+O`;deVq9>K4`*@pNoIdbUpZaxN-8KhR@r}Q7Dx(5ccA7jt1i~&Dr zdE;H&G@1X(Vd{vUbuG$WYzu%9d&==^&$Q-P6G+lWvNDR=$gng<84O5LSDaV(6%phi z{<=$1!HnS}9o=EEd6M4v)7MPF4g{|)R26SfrM>0z^T9n7E#B%q$*{)|U!0}O;oi6+ zb*BF)#vT;p!^VqFkZgo)->$byR|~aqFg+N<<~`HbLy(^4YA;x#qT-G-j0@Rv5^YXv z)8Y}u_3z*AXD)=2kNvP>#VR)18RXb&G_uBTFyic1)WqFq&>A~!vq35-lyXf$Fi}|f znEk*$#1b4NGX3kAH}#g_12UVj^Ur8!sI+b_h zgoH8_Xr3j`R%x~M5ok1$1_;ig4Yrh5|62{PByd&C!w`G3MFX*EI zVi=%?lk~o8CLNVc#rJbvh%dc*oT)NdgTZFbxe2l&6_D|i6fg-s*{y|SlrJ$ZJ$Q8g z?pN{zFY{iCeT6E9G2Ni|hmubRF+~LjOP7G9^;vhxR47CiLbEd8&t4M3NXiux%vK0O9w-0|f4jjF@)CqRRiZ%tC{I*ryh-=$@>vdmN` z@jEhN#BhSUqbJ#h1gdLr_1qC&Nhu7U-ujG>L)YH~sF;0UHHr>LxSElYd^2RrXY9hter5as>R&P4H98$A1c~pP}w| zCo}2eH^pb_Aa9VWU^w3=4xaNTm8~SJ?Lr49Q&Zk#C_6shZWMOTDx~$j>H-2@&Bk62 z8R1(*@?Gw&g19Gx)Lqm2B|cM6;WP}e9McAIiY3a!X5P!g!dOu$D7gkq@ZzGz^b?0B z0>(XkmI&Q>b-2pib3mAvGHFo#8wkfB0}EbU0*c?R``@}Var8V&ah$1bCB-T=)wz!m zm7EIeT4YP{wgP31qbeP{+5JoC(!IJ(-*c->?R#X=(<_k8~9c%z!Ndqt~7_25|f2hs>9Eb2kd92 z9er-D0;2Gh0H6X+$L>uq1iYc`B3et(cAk!R2|rUZg0tV#yD8E<3YThFfyXkAXvHbQ zao&b@x%blgAd+7A<`C8MB<D$Wr+(F12{cF=}Zik4a3HSCKTLcIK3t-iki z@w+yu@>mOjQL(ml&b17NkXhh$*3c-dcs_ ziSF@7z!~>Kha1{%ez|87Qw53^o8b1E&FMkOGoYX7qfro6_V9@zZlcgKX-jz&#X?iL zpqsfaR9c`#!IJWa!sqlAgm-|Q~x!ECW>B{_gZ6j-w938B38D@^0(3IGe?>?*by2O9EN~94{j!TW$=0&6U2n zxM8RznM=$&^bR9l0j{#-AI$HY*L3p{J!iU?5U-M8>Tg5JWG2oX^QY$yt)WLlA9zlG z*5l0g`b#Q+P@HIT;BT6uO$E0zfj++(YDL4memy>AGi_a#(qB6H}6lSlLcln1JF!Je+f>w zQLPli$^7b&QL8YT{xz7OlLGH!ToX15-|yiVnH#Dm)?yk)GMGL$0_H2r{VW}ElZoHP_tTm7qm?DKeHzEA)pJ<&lyCz3d} z16#d7JW0_1469BpI|u&|mOdBiS!6S()sI|gU9%_ypb*>{?=3og2F_{ z{aevjrq}rMu!n)I>e*rGCO z>eGB*-Uugcizxtkoxr$SdD!4;H)c|J;al#$ad?%Qzsrxjp8>bLfa!Z@eaOr7Mqa9V z10*=_Iu7AW8`rZ3=C~ETF7WCtH$?8?GQoOH7QD!}N7&=^^{2NuedZjj@u>#}ibMD- zr3#peI5FT)BNzSPy*1iMYQ1cSw?XnftEt7r_Djn0Abrr)KCfAu5vACW(|WSoY(Ik2 zZZB=6(_JfB9OMCVA%mwO`U;q5%+^3NV|=7&YL$kvWAZ$lE3@3d(<=&t1Kau?v#0>) z&*oF2&Pf0+TwlFthvu5I&vZ{Pt)KxOyI1DNGjXK@9XI`b;JBD%1jQVm#UsO6){g^9 z+5cox-qtaYjc`6Taol(o)!LRYHor&%Wpp~1m)tE>^G*Vv>OIE~giaf{Yb9tIcdY0eiCG@e4UO0T^SWa| zt@>@j3PzH7kJPXu=I)651(sjXX9>fP!~H98BPD@#oT`&wn=OHz;Uc^&>Gmsbd-P@l zqb91>lO2*BQ4hk6Nf>pWQl%CHPp$g6mXN$xAr0TgGGa zRiH(xGD9v6Yomh`K7@#0L`^ysNVSy`g-ff1TQ@-#X(PEKEma<6#@+lktoi73J|_t0 zjJ@%b5xgDj~euI&SMb3HdEHw+OYck-yFeGEpu} z-@EQuwY}8K^|ED6CMB{-6!Fq{DE%}?mtuDmCu`wyi&)Gur&M;HV19jm`>krUK`6RR zDQd}LHWn_i&$c@Pj*ogAO+&5U{wa`P$@V*9LtFQre);Tqu91_XmH)s>CAEWI--Dhi zj^~Ri+(G?5t(Xa;5O-jUmGj|WVV{~#n!PyU&c613C+?Ww zwK>|skjK<==^B3wv@5!`AEo-%8NR4BCa$c|+h^q4eh7NZwGo*L4w?1d#(J~0AqykrF&(H^eCOrdKokJQf47Lc z0;R}8o@jh8w2dik0y7mXiomOI3lVjr+&iM9X#a0)=)H8Y_<1ofFbGJv*pQqwZ;y%z zG|a44lWH!q-I| zHx0AKbk}MYn0#xV%AI6D^$u@&Thh{hjMK zBP-K;f!8epmZlslyG!X%BmRU4JQ-E`c?cH(MgEUGdWWp>FRXP67Kv6LKkuU*4ak(* z$%morx|3TDmtP}3esW@)y2d=KdyN$ozkkkrs^uh*C*ah^TVw_9XHG@5zY)Y+8ZIX8 zb3>D{zS=8PzQMIBL8l}XHUtE=?tQJG+e+Hfh)N`e&;>#@kGFG=Lu4+R1NcWDml=t_ zP!v_~d&kX3Zl3EtfjgMF`%&!o$cj%k8kt~HBuo>G)){`STE`E+7jT;!yzFUesxluR zPRBJh_(FoRS6Uru&9(|1jyqBTU%9IrPllrfl8bc#mUwsw;XK-p~| zi8<^X0_Xlbs%IH6-7r??`b3@FXSv^#T-iY!O%IL1LuC_{na%^unuNM=O;T>@m8dE& zU&7{+d)s8qQF>G^WVUz>qeG_|`M^6oAP@4$ zW-MIi)EepjYp!FOup! zK_Eh8L7Xg*z10D*q0)6pO_JH}L8f!Higw@lkz5s>_#;grO3C~bjOnt6IN%arD}kaF z(wDKMLN(-)-qys>NiVFSjsZ*hIFlg{>N%eiBeRWc`6M{1#qRtq%0R;yG0tNvA)U$v9ISdduDFO=%m9qc@pqv4;g986% zTw;I&0SRtzX>(;RVrgy)0~2;fzG{YHSswx2c+>0}SR0XtmO|KGSOWHD++NezEb*C! zYJ!r0)w9u6+1<86o}*>=(2fz4+J-oqaH;t)CkXQ^^&zj|^=(VLqoR#^Md<&=Xo`me zLQ?ciVOn}dk`EZT9I-5>p^!7vc#?Zi8Nsc`sATN8Ee>NMv$%(Xpx3L1LF?@F(k}hY zQ;7`xGs6{9)Ep>rXOH@)MhOYzYZP5n)_n|Wi9i!m?NpBRO6g^$)q`e>T~k1zp%|Ve zFIySO%K%DA&ZO1B|?NcJs zv%6XCAIpQx#XMeXW(RQ8nN5}-k@u8H0UcF=cyBH^J9Vr)LF6slA{dGpHH;CMC%G1BdwL|+?DlXWTqZI4tiEUtkg~)a1V`e75;G_-UvQXDy;YM z7_09h<2;5^dS8lsP}Pau`tlAO|0iQ{#u#EGj~NCGP;vU!YEr;2_#bvZLkm$&zs@0L zj?iIj=dv?R0SY-JvJNktV-H1MWKSvq8dN1DjR%q3-K;OBL1xqn%vFnxgBun2g!Q#P z?7(|mtl^~bgsW6v{Nb<0M`>G?BU~q4dYa&tSC{Bjr@n4(1mgGg@fp#!%Z}9>!BFkA zw20pyI1VH*mCXKULu}5KX<&!;=24gGcfOgdnJ%$pe*sr+-^+zAU>WZH;AM&HQskVF zeY%}NCe4z#_TA7jAi!Q=$}XE`B@AT9J>F95a5852zp@{i$Jc_eLT%ILu@K2|hDeP1 z)GZUhHe&+4K4#q>4Y*`e{AL4YXM#C7kiI-) zVmgv%57|Vw32zUI(01TVF=|%eeNnk%1xljNx<>J=K($?ksHSHAZ-Ku^fhrjpR|2iRuz-{{Af=Fy%q4A<#u1}K#Vi!F~1S= z%%eJ;PpO3BV%=7#+t{5061)nJ{shVKC6A#QCDkLS4Xx2{%%nh7l#h|4#K!Cv9ajHu$4(4-W zcSdWuJkWO%J`6xY&8p3)Ra-^RP=3}yI{;K^SuM}G>NC-4dPn3p`;Xzeh-&UT?k8;g z2|0`GbGkgqAPL;-=pRM}!V$*WTjYS~O)7w-J4==N$3&>unx&CWL8zCUL+rg2yu1&r_v4jiNB@B}XyHV7d$yGuA37eSF&^#P*bq$;?- zFe*a_`g|QzeDwZV(Zz9R#@zOfsTTH(Sh_lRMGOUxkjigX*@Zv6vsRcp?B){T;r+9m zIEt1HA~ShD6az}7FU9v5EvR-+wJI;PhN0;!y8x!Xo19&5aD?0c5@r68^}422R!3wm zS&6v9Py#STuUeAW#p;(vTlkqK3c42xpy)Bj*#Ux~3bDN>Xgn2(bR(=;}>X>UDS zM9&skmMev|%f|}7hS-7q<#TXcw)<0Njjk6Edo>&^T3%!!0-EJcOXi*9{Bt70+#1BfBq>! z%<6a`;3g~hvJRQCtRj4|y~vKXjC_{T{Un>3wkv*_c z%ZF3eZztr^I8|IJxF06|_MzRYHaoGp42pLZoLozG{+Zjyl9ArJsntMIcGvG*$ zH(e8B-A1tb(f9i|&4$1Ost34MIN_Jr)GUPuhfvfZGOkLguslOr3hSvrT5yy?lVliS z-PKz&JO27qGN29!B4DpYFT}1mi=7Cndnc%mH!-lOSpd0x{}xyp50Ag?lSp=iyU`nq zRni|_h71Kjn|oRZ)51Jbu?nZOjC#0AsbTrpyKx%su$_O*aOj>^rBJD2d(+ed2Y0O|7A$p8F^lLf%rVa?rXMqbSGG`B86jr1 zT5Q4j60W{GFPLRPNFB}|Z^g}BYIuYy+G>(~05>&wSi`-O4!3e!D?Nyu>~pe~~J2z+qV`oL&}A!1B!PnfT&Qo-DYB^eAv(8mpp_q&uhw-Cz(pb^37ae&S(rWB0Br9ZtvirD?dyne?&~(`CA7qNYG}? zH5QeuRgea)75I-&^}9Y;^OqgDS)@`VFaT<(P_Z6%DY;f^%sO*4*3*AR^h4QnG?iy; zS8w7EbV~wkc7J{PF;Ab(yJmBlFhC1JLt%OZV!CyLAIKaUd>U%L_xEykJkNg4Lz*1ij106wvV! zR%zY>h>pnR_BBwUw{-r1RBZgj8K9t2?{4s3BC!{{qxO`6$=|xDf=OqHIAgC%9BPKr z=KAEh-)kSlAK-~8uqK(pF>Ct`>I{e~q`n<9PZM~2=D-L-MwK7Lh7Ph=T|{jspj*km zb}8V9=yDcM_QOvOq6e{%h=KuP zHIH|}-GWb$jw&cs0^SY8IEgZuUsP{H?sw&+QGmIZI&4WTackXvttR^p6zK3DJ8x0s zo~)q=%g2sXaA3$-7f17M-IBAu7O%kRgDJLVpti?QguQe2vxb4_U^X$R+)&KPQ4R(3 zF*fryRVi9g3mDkO;QS>_37$%7XNG6%)K+m2if8>yd$V!=_A4~AC28+HqbD<5?;6#U$}T=toiGAhrB?n$-+sMt zm5bl%XhPLxs|v^q88#@hEM^sjDEWDR$}|E!BycR1;@ma-$isk}tQ&W@y~$}ZrRkyd?e|ht4#gRO^q8a3ZVAyquf28O$Cej{wOng zPKqtQD<5ur>O|6+)YkWI5dPGz>p(V7^QDKJ&ifG9C~QN@zKeQ!)Dk%5dj;(km}ylC zG+fH*o3`XJs?FaArDA{p@y>3zg9{y)dOb1W#wCkuib9Sr#vF$I`oza_9j*Wlz+_sO z{^_6~s_gTW&Gq1Rj*x zSBj!d1@R}h(GRNr(1Ar z30Um?>)61Fw6$sB!G{~J)NB4;(1Fd>C8?1=*mIrmi(_PvP}6+9yfV~DS45JOVYV&7 z+df;nGQ03VXuT`jJXN1~V*rTTfX;!r*Gl_;N#UisPf!kiZ=qY2>H&NTIuXiUj4LtjgO=_ z_Yv~w{!Rw;REHO#Hlya&tV9@frE~vy5aqu`32s=A=I$gG9=<|PPi#*A2 zT%Y_E4Lw_b(%T&V+TBlkGN9z;`zaX=C}N0&QPWYZuW0|X&g;az;zuStey54Qz!o71 zd)Czta=KB1gbPY-f9Y1qGt~o+FUR#wl0jTDu4wN^jb^HkBdh8J#eZH*JJ4$xa^VkNR=>lew7SoU0cZTMC6-PXU$M=yIe@Ui<~6;>m`7=URa4LnPZ%dH;IcNP zRDDWACm^md3RvR6x@j^t@#TE?$OAgIZiy@Dm+U#jucx<*#cDCe||fQNm~>rYyr$?)@bvl%mg<-$ZxL z@qfc2Kc7aeRo-Vuk{P&)j1Hd4E0`8OjUw~o3`BU0GD5Mtc;38l9Gti z;jSc?@0|q3I%WW@PlABZm${1duxOKC%yzN zwcxJG6fC6_ndgLavm{1J+19Shx~BC^9(bs%fw}P&Hl^07tDSz{&o8Zm*!xgQ8lpiv zjOEZu-CkSf6f`FBM%k-dwzxl3ROl*KTU6fCtEb+m4Qt2E@^4&=?-EH*vbqr9>Ma5$ zdjOVx7-m!1T4G=_U&Fdxx1 z)?bAUsS_}3x>Q$JRs{eSLZ&t8j2%&Rf_W&F`LEAwk$HIUJx*@v@{!3^FRf_!P<#gx z&QzY2R$-E?hG85&2jkLscfIO*YCT^S3?$O7Gj8+o!$#XJ!*_e-?eWpArk{$@HHVUx z;ens0Fi0Vq&hADvh%tPJi2sGvO2TseRLzFJEP-@6kEG*T3-drd)#xTVa zl2Yb~&r!dq;K2~x&9I-^s2s@B1V_``1xfWXvThA>@P0wIO>^AkcR=;3Huo}Id6N)d z_o=SuX{QmvsHwD##zm`B^{aG0Odx1NAnY3?iD?Lis7#qQjBG$-L8g{zwB$U2RDPWT zoGzV}9@_VZJ(%mhMmaD( z%)`Hpzao^L0RMB59LM-S~6yCjKNp&AltDmtmUKdD`f?{R+LWjV`qvlMbl3MtutHfGw@DY?pZa9mO zi0=HhgMO}>f0NesUe{RSXcN|KML6=*F!wel7qS+5XILL%51hOCFFy`A)=Xoj`=Niv zT3q>`gnPWJzAC5OV$%uVEu4=XUrEbb2~EVp=2#9q?F=^!kDlxw2a4P;)!e`5A7PXy zizX&V6-2`>We>-GvqH<;VDH#tfWd;*8I^D#<*I%|09*fa)d!W#J!^ZV=G@6}$I~^b zY~Xr!2!l+7IK#5-+53m=50(}&LNPy}5~gn)zDpA(OZ?cZ8y5l}y!BI3;~recjP)Oj zqte=!#!M%9?4kLe+PdESf{?Q1Ayj*4biB3;PYI$T3ii99vIe5wE>G?B+_7BVl2cfd z`RKGzLwump(%u{E*Q>JDe8#Gs@dV~+Gpo{3Jo`3aN$F%bb~|s!=*S;eIC5gsu%XWaBRiLw);7DIEE&uT`6qtZG=ucRw@~$+j%ksf z3`fQOHw3qLx0$+)BpL@xlBm~lNp2%5cnU#NAvdA_a*(>XGZvJuSmH17x_i%F%Zeb< zZitTdmSHXtMrm9j`Z!nJ-3jNL9V^(2(jky9RLH7sRCC_DXnvEN4r{~DVIm85#k|!9 z!tX6AX*WTNFMxP-Q;5hMoD@7vPda@To(2FR`Lw zCqhpZ29wepDJgZHeaRC2WxsY)|I4(RYG5{8KEU27i@i01u$fbAWliss7N z-|)1GG*z&85u`r$LilV!n<5SoPOqhWIpf~sRl{g1`b%rC;+6iSM_9TNy;C@CcIFiF zp#1lXd}^xsaIglGZNQQ#oR#_4ezuqD{Qq@A+?(q6|3Q-$H=gRO>2R5&t~Qqlo8FT5dUKCeTt*fQ2a`D(PW(?!%UQ+zV=CRGQc)^xjtAPbarHIkXn&w_ zZBtW$fFenU}ULq)d^l}SqasDA_?!O-%4`OmtfrpXmyFjtlSfM6(jf8a_VqbUOfgJ%)n zY8WU01mFhSl@uIpIrs;K_Jm!xm`UBA|Ep_8bmMeT?<@ya1jF_0^LvJ_T1ewQMBell zl?TIbZz!fx9tZj5ocEnl66v5qrL|aTCf$KMyl|^$8J#pC=}%@YD}0q5jFTT7Ze0M~ zbTXe|q1ELezhQe+5#dMR-YP)*K~x}YYaA*L0|vJM0Ji`DfB*pkQBMiT8X__P!!s^9 z3=lXe0t*Y3vj7C3oB_0h0{>=QVt@ky32tv`b7d}KX>JMw6Lv?wYKCE1A02mAQ3C}8 E00+YImjD0& literal 0 HcmV?d00001 diff --git a/crates/exav-core/tests/fixtures/volumes/split.z01 b/crates/exav-core/tests/fixtures/volumes/split.z01 new file mode 100644 index 0000000000000000000000000000000000000000..24bd10337239c02011fc898d2d6429ddd913addb GIT binary patch literal 65536 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