pyOpenVBA 6.4.0__tar.gz → 6.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/PKG-INFO +52 -1
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/README.md +51 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/pyproject.toml +1 -1
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/__init__.py +10 -1
- pyopenvba-6.5.0/src/pyopenvba/integrity.py +506 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/.gitignore +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/LICENSE.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/architecture.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/excel_checklist.csv +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/host_completeness.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/power_query.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_complex/README.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_designs/README.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_macros/README.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/access_write/README.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/research/pcode/README.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/roadmap.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/docs/vba_runtime.md +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_a1.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_deflate.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_new_project.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_package_copy.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_package_signature.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_references.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_relationships.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/_xml.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_alloc.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_btree.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_compact.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_complex.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_datapage.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_ddl.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_designs.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_facade.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_format.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_index.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_lval.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_macros.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_pages.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_props.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_queries.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_rows.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_schema.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_sql.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_storage.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_tdef.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_themes.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_validate.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/_vba.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access/database.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_events.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_find.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_io.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_model.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_names.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_region.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_io.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_model.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_arity.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/host.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/special.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/values.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_deep.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_display.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_formula2.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_inventory.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_parse.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_prefixes.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_r1c1.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_spell.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_structured.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/formula/_values.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_ast.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_lex.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_objects.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_parse.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_values.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_eval.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_library.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_parse.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_values.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_binary.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_files.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_metadata.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_opc.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_package.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_section.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/workbook.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/__init__.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_pptx.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_values.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/vba.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/src/pyopenvba/word.py +0 -0
- {pyopenvba-6.4.0 → pyopenvba-6.5.0}/tests/fuzz_corpus/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.0
|
|
4
4
|
Summary: Read and write VBA macros and Excel Power Query in pure Python, no dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
6
6
|
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
@@ -993,6 +993,57 @@ decompress and rewrite every retained part and detect such damage. This does
|
|
|
993
993
|
not repair damaged parts. Unsupported packages fall back to a full rebuild;
|
|
994
994
|
legacy binary formats ignore this flag.
|
|
995
995
|
|
|
996
|
+
### File integrity checks and conservative repair
|
|
997
|
+
|
|
998
|
+
Check a file explicitly before or after editing, independently of the fast
|
|
999
|
+
save path:
|
|
1000
|
+
|
|
1001
|
+
```python
|
|
1002
|
+
from pyopenvba import check_file, repair_file, FileRepairError
|
|
1003
|
+
|
|
1004
|
+
report = check_file("book.xlsm")
|
|
1005
|
+
for issue in report.issues:
|
|
1006
|
+
print(issue.severity, issue.code, issue.location, issue.message)
|
|
1007
|
+
|
|
1008
|
+
if report.ok:
|
|
1009
|
+
print("All supported integrity checks passed")
|
|
1010
|
+
elif report.complete and report.errors and all(i.repairable for i in report.errors):
|
|
1011
|
+
try:
|
|
1012
|
+
result = repair_file("book.xlsm", output="book.repaired.xlsm")
|
|
1013
|
+
print(result.changes, result.after.ok)
|
|
1014
|
+
except FileRepairError as exc:
|
|
1015
|
+
print(exc.report.issues)
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
`check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
|
|
1019
|
+
`checked_parts`, and `complete`. `ok` requires no errors and complete coverage
|
|
1020
|
+
of the supported checks. ZIP parts are decompressed and CRC checked; local
|
|
1021
|
+
headers, data descriptors, XML, content types, and internal relationships are
|
|
1022
|
+
checked too. CFB directory pointers and stream sizes are checked, and supported
|
|
1023
|
+
VBA projects have their module sources and forms parsed. VBA is never executed.
|
|
1024
|
+
These checks do not prove that Office can open the document, compile its VBA,
|
|
1025
|
+
or verify its digital signatures.
|
|
1026
|
+
|
|
1027
|
+
Repair initially supports **stale local ZIP CRC/size fields only**, when the
|
|
1028
|
+
actual payload passes central-directory verification. It preserves every
|
|
1029
|
+
other byte and checks the candidate before writing a new file. It refuses
|
|
1030
|
+
missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
|
|
1031
|
+
`FileRepairError.report` explains the refusal. An existing output is never
|
|
1032
|
+
overwritten, and a healthy input can be copied without changes.
|
|
1033
|
+
|
|
1034
|
+
The default limits are 256 MiB per uncompressed part and 1 GiB for both input
|
|
1035
|
+
size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
|
|
1036
|
+
`max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
|
|
1037
|
+
input, missing paths, and filesystem failures raise exceptions. A skipped part
|
|
1038
|
+
is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
|
|
1039
|
+
metadata is currently reported as incomplete. Unknown VBA formats and Access
|
|
1040
|
+
coverage are also explicit warnings: Access checks its catalog and VBA, not
|
|
1041
|
+
every table row or allocation map. Access's existing `compact_and_repair()`
|
|
1042
|
+
returns a rebuilt database and remains a separate operation.
|
|
1043
|
+
|
|
1044
|
+
Encrypted ZIP parts and compression methods other than stored/deflate are
|
|
1045
|
+
reported as incomplete and cannot be repaired by this API.
|
|
1046
|
+
|
|
996
1047
|
`save()` enforces the following protection and signature gates.
|
|
997
1048
|
|
|
998
1049
|
### Password-protected projects
|
|
@@ -956,6 +956,57 @@ decompress and rewrite every retained part and detect such damage. This does
|
|
|
956
956
|
not repair damaged parts. Unsupported packages fall back to a full rebuild;
|
|
957
957
|
legacy binary formats ignore this flag.
|
|
958
958
|
|
|
959
|
+
### File integrity checks and conservative repair
|
|
960
|
+
|
|
961
|
+
Check a file explicitly before or after editing, independently of the fast
|
|
962
|
+
save path:
|
|
963
|
+
|
|
964
|
+
```python
|
|
965
|
+
from pyopenvba import check_file, repair_file, FileRepairError
|
|
966
|
+
|
|
967
|
+
report = check_file("book.xlsm")
|
|
968
|
+
for issue in report.issues:
|
|
969
|
+
print(issue.severity, issue.code, issue.location, issue.message)
|
|
970
|
+
|
|
971
|
+
if report.ok:
|
|
972
|
+
print("All supported integrity checks passed")
|
|
973
|
+
elif report.complete and report.errors and all(i.repairable for i in report.errors):
|
|
974
|
+
try:
|
|
975
|
+
result = repair_file("book.xlsm", output="book.repaired.xlsm")
|
|
976
|
+
print(result.changes, result.after.ok)
|
|
977
|
+
except FileRepairError as exc:
|
|
978
|
+
print(exc.report.issues)
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
`check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
|
|
982
|
+
`checked_parts`, and `complete`. `ok` requires no errors and complete coverage
|
|
983
|
+
of the supported checks. ZIP parts are decompressed and CRC checked; local
|
|
984
|
+
headers, data descriptors, XML, content types, and internal relationships are
|
|
985
|
+
checked too. CFB directory pointers and stream sizes are checked, and supported
|
|
986
|
+
VBA projects have their module sources and forms parsed. VBA is never executed.
|
|
987
|
+
These checks do not prove that Office can open the document, compile its VBA,
|
|
988
|
+
or verify its digital signatures.
|
|
989
|
+
|
|
990
|
+
Repair initially supports **stale local ZIP CRC/size fields only**, when the
|
|
991
|
+
actual payload passes central-directory verification. It preserves every
|
|
992
|
+
other byte and checks the candidate before writing a new file. It refuses
|
|
993
|
+
missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
|
|
994
|
+
`FileRepairError.report` explains the refusal. An existing output is never
|
|
995
|
+
overwritten, and a healthy input can be copied without changes.
|
|
996
|
+
|
|
997
|
+
The default limits are 256 MiB per uncompressed part and 1 GiB for both input
|
|
998
|
+
size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
|
|
999
|
+
`max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
|
|
1000
|
+
input, missing paths, and filesystem failures raise exceptions. A skipped part
|
|
1001
|
+
is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
|
|
1002
|
+
metadata is currently reported as incomplete. Unknown VBA formats and Access
|
|
1003
|
+
coverage are also explicit warnings: Access checks its catalog and VBA, not
|
|
1004
|
+
every table row or allocation map. Access's existing `compact_and_repair()`
|
|
1005
|
+
returns a rebuilt database and remains a separate operation.
|
|
1006
|
+
|
|
1007
|
+
Encrypted ZIP parts and compression methods other than stored/deflate are
|
|
1008
|
+
reported as incomplete and cannot be repaired by this API.
|
|
1009
|
+
|
|
959
1010
|
`save()` enforces the following protection and signature gates.
|
|
960
1011
|
|
|
961
1012
|
### Password-protected projects
|
|
@@ -81,6 +81,9 @@ from pyopenvba.exceptions import (
|
|
|
81
81
|
VBAProjectError,
|
|
82
82
|
)
|
|
83
83
|
from pyopenvba.forms import FormControl, Size, VBAForm
|
|
84
|
+
from pyopenvba.integrity import (
|
|
85
|
+
FileCheckReport, FileIssue, FileRepairError, FileRepairResult, check_file, repair_file,
|
|
86
|
+
)
|
|
84
87
|
from pyopenvba.powerpoint import PowerPointFile
|
|
85
88
|
from pyopenvba.powerquery import PowerQuery, PowerQueryWorkbook, QueryGroup, RefreshSettings
|
|
86
89
|
from pyopenvba.powerquery import pull_queries as _pull_queries
|
|
@@ -228,6 +231,12 @@ def push_access(
|
|
|
228
231
|
|
|
229
232
|
|
|
230
233
|
__all__ = [
|
|
234
|
+
"FileCheckReport",
|
|
235
|
+
"FileIssue",
|
|
236
|
+
"FileRepairError",
|
|
237
|
+
"FileRepairResult",
|
|
238
|
+
"check_file",
|
|
239
|
+
"repair_file",
|
|
231
240
|
"AccessDatabase",
|
|
232
241
|
"AccessReader",
|
|
233
242
|
"CFBError",
|
|
@@ -293,4 +302,4 @@ def push_power_query(
|
|
|
293
302
|
"""
|
|
294
303
|
return _push_queries(src_dir, workbook, out=out, encoding=encoding, remove_missing=remove_missing)
|
|
295
304
|
|
|
296
|
-
__version__ = "6.
|
|
305
|
+
__version__ = "6.5.0"
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""Explicit file diagnostics and conservative, content-preserving ZIP repair.
|
|
2
|
+
|
|
3
|
+
These checks do not execute VBA, verify signatures, or prove that Office can
|
|
4
|
+
open a document. Access diagnostics cover the catalog and VBA, not every row
|
|
5
|
+
or allocation map; use AccessDatabase.compact_and_repair for compaction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# The checker deliberately inspects CFB directory sizes and host format
|
|
11
|
+
# metadata, which are internal to the parsers it validates.
|
|
12
|
+
# pyright: reportPrivateUsage=false
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import posixpath
|
|
16
|
+
import struct
|
|
17
|
+
import zipfile
|
|
18
|
+
import zlib
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Literal, NoReturn
|
|
22
|
+
from urllib.parse import unquote, urlsplit
|
|
23
|
+
from xml.parsers import expat
|
|
24
|
+
|
|
25
|
+
from pyopenvba.cfb import CFB
|
|
26
|
+
from pyopenvba.exceptions import NoVBAProjectError, PyOpenVBAError
|
|
27
|
+
|
|
28
|
+
_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
29
|
+
_CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types}"
|
|
30
|
+
_DEFAULT_PART = 256 * 1024 * 1024
|
|
31
|
+
_DEFAULT_TOTAL = 1024 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FileIssue:
|
|
36
|
+
"""A stable diagnostic code, severity, location, and explanation."""
|
|
37
|
+
|
|
38
|
+
code: str
|
|
39
|
+
severity: Literal["error", "warning"]
|
|
40
|
+
location: str
|
|
41
|
+
message: str
|
|
42
|
+
repairable: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class FileCheckReport:
|
|
47
|
+
"""Results for the on-disk snapshot, not unsaved in-memory edits.
|
|
48
|
+
|
|
49
|
+
``ok`` requires no errors and complete coverage of the supported checks.
|
|
50
|
+
Warnings explain unsupported coverage. This is not Office certification.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
path: Path
|
|
54
|
+
issues: tuple[FileIssue, ...]
|
|
55
|
+
checked_parts: int
|
|
56
|
+
complete: bool
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def errors(self) -> tuple[FileIssue, ...]:
|
|
60
|
+
return tuple(issue for issue in self.issues if issue.severity == "error")
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def warnings(self) -> tuple[FileIssue, ...]:
|
|
64
|
+
return tuple(issue for issue in self.issues if issue.severity == "warning")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def ok(self) -> bool:
|
|
68
|
+
return self.complete and not self.errors
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class FileRepairResult:
|
|
73
|
+
"""The original findings, changes actually made, and verified output."""
|
|
74
|
+
|
|
75
|
+
output: Path
|
|
76
|
+
before: FileCheckReport
|
|
77
|
+
after: FileCheckReport
|
|
78
|
+
changes: tuple[FileIssue, ...]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FileRepairError(PyOpenVBAError):
|
|
82
|
+
"""Repair refused because content or unsupported structures need guessing."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, report: FileCheckReport) -> None:
|
|
85
|
+
super().__init__(message)
|
|
86
|
+
self.report = report
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _xml(data: bytes) -> tuple[str, list[tuple[str, dict[str, str]]]]:
|
|
90
|
+
"""Parse well-formed XML without DTDs or entity expansion; retain children."""
|
|
91
|
+
parser = expat.ParserCreate(namespace_separator="}")
|
|
92
|
+
root = ""
|
|
93
|
+
depth = 0
|
|
94
|
+
children: list[tuple[str, dict[str, str]]] = []
|
|
95
|
+
|
|
96
|
+
def start(name: str, attrs: dict[str, str]) -> None:
|
|
97
|
+
nonlocal root, depth
|
|
98
|
+
if depth == 0:
|
|
99
|
+
root = name
|
|
100
|
+
elif depth == 1:
|
|
101
|
+
children.append((name, attrs))
|
|
102
|
+
depth += 1
|
|
103
|
+
|
|
104
|
+
def end(_name: str) -> None:
|
|
105
|
+
nonlocal depth
|
|
106
|
+
depth -= 1
|
|
107
|
+
|
|
108
|
+
def refuse_dtd(*_args: object) -> NoReturn:
|
|
109
|
+
raise ValueError("DTDs and declared entities are not supported")
|
|
110
|
+
|
|
111
|
+
parser.StartElementHandler = start
|
|
112
|
+
parser.EndElementHandler = end
|
|
113
|
+
parser.StartDoctypeDeclHandler = refuse_dtd
|
|
114
|
+
parser.EntityDeclHandler = refuse_dtd
|
|
115
|
+
parser.ExternalEntityRefHandler = refuse_dtd
|
|
116
|
+
parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_NEVER)
|
|
117
|
+
parser.Parse(data, True)
|
|
118
|
+
return root, children
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _verify_deflate(raw: bytes, info: zipfile.ZipInfo, limit: int) -> None:
|
|
122
|
+
"""Do not trust file_size: ZipExtFile can trim output to that size."""
|
|
123
|
+
name_size, extra_size = struct.unpack_from("<HH", raw, info.header_offset + 26)
|
|
124
|
+
start = info.header_offset + 30 + name_size + extra_size
|
|
125
|
+
end = start + info.compress_size
|
|
126
|
+
if end > len(raw):
|
|
127
|
+
raise ValueError("Truncated compressed payload")
|
|
128
|
+
decoder = zlib.decompressobj(-15)
|
|
129
|
+
size = 0
|
|
130
|
+
crc = 0
|
|
131
|
+
for offset in range(start, end, 1024 * 1024):
|
|
132
|
+
chunk = raw[offset:min(end, offset + 1024 * 1024)]
|
|
133
|
+
while chunk:
|
|
134
|
+
output = decoder.decompress(chunk, min(1024 * 1024, limit - size + 1))
|
|
135
|
+
size += len(output)
|
|
136
|
+
if size > limit:
|
|
137
|
+
raise ValueError("Actual decompressed payload exceeds byte limit")
|
|
138
|
+
crc = zlib.crc32(output, crc)
|
|
139
|
+
chunk = decoder.unconsumed_tail
|
|
140
|
+
if decoder.unused_data:
|
|
141
|
+
raise ValueError("Trailing bytes within compressed payload")
|
|
142
|
+
if not decoder.eof or size != info.file_size or crc != info.CRC:
|
|
143
|
+
raise ValueError("Actual deflate payload size or CRC disagrees with central directory")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _cfb_check(raw: bytes) -> None:
|
|
147
|
+
cfb = CFB.from_bytes(raw)
|
|
148
|
+
entries = cfb._directory
|
|
149
|
+
if not entries or entries[0].obj_type != 5 or sum(entry.obj_type == 5 for entry in entries) != 1:
|
|
150
|
+
raise ValueError("CFB directory must have one root entry at index zero")
|
|
151
|
+
active = {index for index, entry in enumerate(entries) if entry.obj_type in {1, 2, 5}}
|
|
152
|
+
states: dict[int, int] = {}
|
|
153
|
+
# The reader tolerates broken directory pointers; diagnostics must not.
|
|
154
|
+
for first in active:
|
|
155
|
+
stack = [(first, False)]
|
|
156
|
+
while stack:
|
|
157
|
+
index, leaving = stack.pop()
|
|
158
|
+
if leaving:
|
|
159
|
+
states[index] = 2
|
|
160
|
+
continue
|
|
161
|
+
if states.get(index) == 1:
|
|
162
|
+
raise ValueError("Cycle in CFB directory pointers")
|
|
163
|
+
if states.get(index) == 2:
|
|
164
|
+
continue
|
|
165
|
+
states[index] = 1
|
|
166
|
+
stack.append((index, True))
|
|
167
|
+
entry = entries[index]
|
|
168
|
+
for pointer in (entry.left_sibling_id, entry.right_sibling_id, entry.child_id):
|
|
169
|
+
if pointer == 0xFFFFFFFF:
|
|
170
|
+
continue
|
|
171
|
+
if pointer not in active:
|
|
172
|
+
raise ValueError("CFB directory pointer references a missing entry")
|
|
173
|
+
stack.append((pointer, False))
|
|
174
|
+
# Include orphan directory entries, and compare against declared sizes:
|
|
175
|
+
# the normal reader intentionally returns the bytes it can retrieve.
|
|
176
|
+
for entry in cfb._directory:
|
|
177
|
+
if entry.obj_type == 2 and len(cfb._read_stream(entry)) != entry.size:
|
|
178
|
+
raise ValueError(f"Truncated CFB stream: {entry.name}")
|
|
179
|
+
pending: list[tuple[str, ...]] = [()]
|
|
180
|
+
seen: set[tuple[str, ...]] = set()
|
|
181
|
+
# Bound traversal even if a malformed directory cycles through storages.
|
|
182
|
+
while pending:
|
|
183
|
+
path = pending.pop()
|
|
184
|
+
if path in seen or len(path) > 128 or len(seen) > 100_000:
|
|
185
|
+
raise ValueError("CFB directory traversal is cyclic or exceeds limits")
|
|
186
|
+
seen.add(path)
|
|
187
|
+
streams = cfb.list_streams_at(path)
|
|
188
|
+
storages = cfb.list_storages_at(path)
|
|
189
|
+
names = [name.casefold() for name in streams + storages]
|
|
190
|
+
if len(names) != len(set(names)):
|
|
191
|
+
raise ValueError("Duplicate CFB directory names within one storage")
|
|
192
|
+
for name in streams:
|
|
193
|
+
cfb.get_stream_at(path, name)
|
|
194
|
+
pending.extend((*path, name) for name in storages)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _host_check(path: Path, raw: bytes, issues: list[FileIssue]) -> bool:
|
|
198
|
+
# Imports here keep the diagnostics module independent of the public facade.
|
|
199
|
+
from pyopenvba.excel import ExcelFile
|
|
200
|
+
from pyopenvba.powerpoint import PowerPointFile
|
|
201
|
+
from pyopenvba.word import WordFile
|
|
202
|
+
from pyopenvba.vba import parse_vba_project
|
|
203
|
+
|
|
204
|
+
plain_formats = {ExcelFile: {".xlsx", ".xltx"}, WordFile: {".docx", ".dotx"},
|
|
205
|
+
PowerPointFile: {".pptx", ".potx", ".ppsx"}}
|
|
206
|
+
for host_type in (ExcelFile, WordFile, PowerPointFile):
|
|
207
|
+
if path.suffix.lower() in host_type._zip_formats | host_type._cfb_formats | plain_formats[host_type]:
|
|
208
|
+
# Use the immutable snapshot, without reopening a potentially changed file.
|
|
209
|
+
if raw.startswith(b"PK"):
|
|
210
|
+
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
|
211
|
+
if host_type._vba_entry not in archive.namelist():
|
|
212
|
+
return True
|
|
213
|
+
project_raw = archive.read(host_type._vba_entry)
|
|
214
|
+
else:
|
|
215
|
+
project_raw = raw
|
|
216
|
+
if host_type is PowerPointFile:
|
|
217
|
+
from pyopenvba._ppt_container import extract_vba_storage
|
|
218
|
+
try:
|
|
219
|
+
project_raw = extract_vba_storage(CFB.from_bytes(raw))
|
|
220
|
+
except NoVBAProjectError:
|
|
221
|
+
return True
|
|
222
|
+
cfb = CFB.from_bytes(project_raw)
|
|
223
|
+
if not raw.startswith(b"PK") and host_type._project_storage is not None:
|
|
224
|
+
if host_type._project_storage.casefold() not in {name.casefold() for name in cfb.list_storages_at()}:
|
|
225
|
+
return True
|
|
226
|
+
_cfb_check(project_raw)
|
|
227
|
+
project = parse_vba_project(cfb)
|
|
228
|
+
for module in project.modules:
|
|
229
|
+
# Source is lazy: force every compressed chunk to be decoded.
|
|
230
|
+
_ = module.source
|
|
231
|
+
from pyopenvba.forms import read_forms
|
|
232
|
+
root = () if raw.startswith(b"PK") or host_type._project_storage is None else (host_type._project_storage,)
|
|
233
|
+
read_forms(cfb, code_page=project.code_page, root=root)
|
|
234
|
+
for message in project.validate(cfb):
|
|
235
|
+
issues.append(FileIssue("vba.structure", "error", path.name, message))
|
|
236
|
+
return True
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _package_xml(parts: dict[str, bytes], names: set[str], issues: list[FileIssue]) -> None:
|
|
241
|
+
parsed: dict[str, tuple[str, list[tuple[str, dict[str, str]]]]] = {}
|
|
242
|
+
for name, data in parts.items():
|
|
243
|
+
if name.lower().endswith((".xml", ".rels")):
|
|
244
|
+
try:
|
|
245
|
+
parsed[name] = _xml(data)
|
|
246
|
+
except (expat.ExpatError, ValueError) as exc:
|
|
247
|
+
issues.append(FileIssue("opc.xml", "error", name, str(exc)))
|
|
248
|
+
if "[Content_Types].xml" not in names:
|
|
249
|
+
issues.append(FileIssue("opc.content_types", "error", "[Content_Types].xml", "Missing content types part"))
|
|
250
|
+
if "_rels/.rels" not in names:
|
|
251
|
+
issues.append(FileIssue("opc.relationships", "error", "_rels/.rels", "Missing package relationships part"))
|
|
252
|
+
for name, (root, children) in parsed.items():
|
|
253
|
+
if name.endswith(".rels"):
|
|
254
|
+
if root != _REL_NS + "Relationships":
|
|
255
|
+
issues.append(FileIssue("opc.relationships", "error", name, "Invalid relationships root"))
|
|
256
|
+
continue
|
|
257
|
+
if name == "_rels/.rels":
|
|
258
|
+
folder = ""
|
|
259
|
+
main = [attrs for tag, attrs in children if tag == _REL_NS + "Relationship" and attrs.get("Type") in {
|
|
260
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
261
|
+
"http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument",
|
|
262
|
+
}]
|
|
263
|
+
if len(main) != 1 or main[0].get("TargetMode", "Internal") != "Internal":
|
|
264
|
+
issues.append(FileIssue("opc.main_relationship", "error", name, "Expected one internal Office document relationship"))
|
|
265
|
+
else:
|
|
266
|
+
parent, leaf = posixpath.split(name)
|
|
267
|
+
if posixpath.basename(parent) != "_rels":
|
|
268
|
+
issues.append(FileIssue("opc.relationships", "error", name, "Invalid relationships part path"))
|
|
269
|
+
continue
|
|
270
|
+
folder = posixpath.dirname(parent)
|
|
271
|
+
source = posixpath.join(folder, leaf[:-5])
|
|
272
|
+
if source not in names:
|
|
273
|
+
issues.append(FileIssue("opc.relationship_source", "error", name, f"Missing source part: {source}"))
|
|
274
|
+
ids: set[str] = set()
|
|
275
|
+
for tag, attrs in children:
|
|
276
|
+
if tag != _REL_NS + "Relationship":
|
|
277
|
+
issues.append(FileIssue("opc.relationships", "error", name, "Unexpected relationship element"))
|
|
278
|
+
continue
|
|
279
|
+
identity = attrs.get("Id", "")
|
|
280
|
+
target = attrs.get("Target", "")
|
|
281
|
+
if not identity or identity in ids or not target or not attrs.get("Type"):
|
|
282
|
+
issues.append(FileIssue("opc.relationships", "error", name, "Missing attributes or duplicate relationship ID"))
|
|
283
|
+
ids.add(identity)
|
|
284
|
+
if attrs.get("TargetMode") == "External":
|
|
285
|
+
continue
|
|
286
|
+
if attrs.get("TargetMode", "Internal") != "Internal":
|
|
287
|
+
issues.append(FileIssue("opc.relationships", "error", name, "Invalid TargetMode"))
|
|
288
|
+
uri = urlsplit(target)
|
|
289
|
+
resolved = posixpath.normpath(posixpath.join(folder, unquote(uri.path)))
|
|
290
|
+
if uri.path.startswith("/"):
|
|
291
|
+
resolved = posixpath.normpath(unquote(uri.path).lstrip("/"))
|
|
292
|
+
if uri.scheme or uri.netloc or resolved.startswith("../") or resolved not in names:
|
|
293
|
+
issues.append(FileIssue("opc.relationship_target", "error", name, f"Missing or invalid internal target: {target}"))
|
|
294
|
+
elif name == "[Content_Types].xml":
|
|
295
|
+
if root != _CT_NS + "Types":
|
|
296
|
+
issues.append(FileIssue("opc.content_types", "error", name, "Invalid content types root"))
|
|
297
|
+
continue
|
|
298
|
+
defaults: set[str] = set()
|
|
299
|
+
overrides: set[str] = set()
|
|
300
|
+
for tag, attrs in children:
|
|
301
|
+
if tag == _CT_NS + "Default":
|
|
302
|
+
key = attrs.get("Extension", "").lower()
|
|
303
|
+
seen = defaults
|
|
304
|
+
elif tag == _CT_NS + "Override":
|
|
305
|
+
key = unquote(attrs.get("PartName", "")).lstrip("/")
|
|
306
|
+
seen = overrides
|
|
307
|
+
if key not in names:
|
|
308
|
+
issues.append(FileIssue("opc.content_types", "error", name, f"Override refers to missing part: {key}"))
|
|
309
|
+
else:
|
|
310
|
+
issues.append(FileIssue("opc.content_types", "error", name, "Unexpected content type element"))
|
|
311
|
+
continue
|
|
312
|
+
if not key or key in seen or not attrs.get("ContentType"):
|
|
313
|
+
issues.append(FileIssue("opc.content_types", "error", name, "Missing attributes or duplicate content type"))
|
|
314
|
+
seen.add(key)
|
|
315
|
+
for part in names - {"[Content_Types].xml"}:
|
|
316
|
+
if not part.endswith("/") and part not in overrides and part.rsplit(".", 1)[-1].lower() not in defaults:
|
|
317
|
+
issues.append(FileIssue("opc.content_type_missing", "error", part, "No content type declaration"))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _check(raw: bytes, path: Path, max_part_bytes: int, max_total_bytes: int) -> FileCheckReport:
|
|
321
|
+
issues: list[FileIssue] = []
|
|
322
|
+
complete = True
|
|
323
|
+
checked = 0
|
|
324
|
+
try:
|
|
325
|
+
if raw.startswith(b"PK"):
|
|
326
|
+
parts: dict[str, bytes] = {}
|
|
327
|
+
total = 0
|
|
328
|
+
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
|
329
|
+
infos = archive.infolist()
|
|
330
|
+
names = {info.filename for info in infos}
|
|
331
|
+
if len(names) != len(infos):
|
|
332
|
+
issues.append(FileIssue("zip.duplicate", "error", path.name, "Duplicate ZIP part names"))
|
|
333
|
+
for info in infos:
|
|
334
|
+
name = info.filename
|
|
335
|
+
if (info.orig_filename != name or "\\" in name or name.startswith("/")
|
|
336
|
+
or posixpath.normpath(name.rstrip("/")) != name.rstrip("/")):
|
|
337
|
+
issues.append(FileIssue("opc.part_name", "error", name, "Ambiguous or invalid package part name"))
|
|
338
|
+
if info.compress_type not in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED} or info.flag_bits & 1:
|
|
339
|
+
issues.append(FileIssue("check.compression", "warning", name, "Encrypted or unsupported compression; payload not checked"))
|
|
340
|
+
complete = False
|
|
341
|
+
continue
|
|
342
|
+
if info.file_size > max_part_bytes or total + info.file_size > max_total_bytes:
|
|
343
|
+
issues.append(FileIssue("check.limit", "warning", name, "Decompression byte limit exceeded"))
|
|
344
|
+
complete = False
|
|
345
|
+
continue
|
|
346
|
+
total += info.file_size
|
|
347
|
+
try:
|
|
348
|
+
# Read in bounded chunks; do not trust declared uncompressed sizes.
|
|
349
|
+
chunks: list[bytes] = []
|
|
350
|
+
count = 0
|
|
351
|
+
with archive.open(info) as stream:
|
|
352
|
+
while True:
|
|
353
|
+
chunk = stream.read(min(1024 * 1024, max_part_bytes - count + 1))
|
|
354
|
+
if not chunk:
|
|
355
|
+
break
|
|
356
|
+
count += len(chunk)
|
|
357
|
+
if count > max_part_bytes:
|
|
358
|
+
raise ValueError("Decompressed part exceeds byte limit")
|
|
359
|
+
chunks.append(chunk)
|
|
360
|
+
data = b"".join(chunks)
|
|
361
|
+
if info.compress_type == zipfile.ZIP_DEFLATED:
|
|
362
|
+
_verify_deflate(raw, info, max_part_bytes)
|
|
363
|
+
elif info.compress_type == zipfile.ZIP_STORED and info.compress_size != info.file_size:
|
|
364
|
+
raise ValueError("Stored payload compressed and uncompressed sizes disagree")
|
|
365
|
+
checked += 1
|
|
366
|
+
# Keep only XML for cross-part checks; binary payloads need no tree.
|
|
367
|
+
if name.lower().endswith((".xml", ".rels")):
|
|
368
|
+
parts[name] = data
|
|
369
|
+
if data.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
|
|
370
|
+
_cfb_check(data)
|
|
371
|
+
header = raw[info.header_offset:info.header_offset + 30]
|
|
372
|
+
if len(header) != 30 or header[:4] != b"PK\x03\x04":
|
|
373
|
+
raise ValueError("Missing local ZIP header")
|
|
374
|
+
flags, method = struct.unpack_from("<HH", header, 6)
|
|
375
|
+
crc, compressed, size = struct.unpack_from("<III", header, 14)
|
|
376
|
+
name_size, extra_size = struct.unpack_from("<HH", header, 26)
|
|
377
|
+
extra_start = info.header_offset + 30 + name_size
|
|
378
|
+
extra = raw[extra_start:extra_start + extra_size]
|
|
379
|
+
extra_offset = 0
|
|
380
|
+
zip64 = False
|
|
381
|
+
while extra_offset < len(extra):
|
|
382
|
+
if extra_offset + 4 > len(extra):
|
|
383
|
+
raise ValueError("Truncated local ZIP extra-field header")
|
|
384
|
+
kind, length = struct.unpack_from("<HH", extra, extra_offset)
|
|
385
|
+
extra_offset += 4 + length
|
|
386
|
+
if extra_offset > len(extra):
|
|
387
|
+
raise ValueError("Truncated local ZIP extra-field payload")
|
|
388
|
+
zip64 |= kind == 1
|
|
389
|
+
if flags != info.flag_bits or method != info.compress_type:
|
|
390
|
+
issues.append(FileIssue("zip.local_header", "error", name, "Local flags or compression method disagree with central directory"))
|
|
391
|
+
if flags & 8:
|
|
392
|
+
if crc not in {0, info.CRC} or compressed not in {0, 0xFFFFFFFF, info.compress_size} or size not in {0, 0xFFFFFFFF, info.file_size}:
|
|
393
|
+
issues.append(FileIssue("zip.local_header", "error", name, "Local data-descriptor metadata disagrees with central directory"))
|
|
394
|
+
offset = info.header_offset + 30 + name_size + extra_size + info.compress_size
|
|
395
|
+
expected = struct.pack("<III", info.CRC, info.compress_size, info.file_size) if max(info.compress_size, info.file_size) <= 0xFFFFFFFF else b""
|
|
396
|
+
if zip64 or compressed == 0xFFFFFFFF or size == 0xFFFFFFFF or not expected:
|
|
397
|
+
issues.append(FileIssue("check.zip64", "warning", name, "ZIP64 payload checked; data descriptor not checked"))
|
|
398
|
+
complete = False
|
|
399
|
+
elif raw[offset:offset + 12] != expected and raw[offset:offset + 16] != b"PK\x07\x08" + expected:
|
|
400
|
+
issues.append(FileIssue("zip.descriptor", "error", name, "Missing or inconsistent ZIP data descriptor"))
|
|
401
|
+
else:
|
|
402
|
+
if zip64 or compressed == 0xFFFFFFFF or size == 0xFFFFFFFF:
|
|
403
|
+
issues.append(FileIssue("check.zip64", "warning", name, "ZIP64 payload checked; local size metadata not checked"))
|
|
404
|
+
complete = False
|
|
405
|
+
elif (crc, compressed, size) != (info.CRC, info.compress_size, info.file_size):
|
|
406
|
+
issues.append(FileIssue("zip.local_metadata", "error", name, "Local CRC or sizes disagree with verified central directory", True))
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
issues.append(FileIssue("zip.part", "error", name, str(exc)))
|
|
409
|
+
_package_xml(parts, names, issues)
|
|
410
|
+
if complete:
|
|
411
|
+
if not _host_check(path, raw, issues):
|
|
412
|
+
issues.append(FileIssue("check.vba_scope", "warning", path.name, "VBA validation is unavailable for this extension"))
|
|
413
|
+
complete = False
|
|
414
|
+
elif raw.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
|
|
415
|
+
_cfb_check(raw)
|
|
416
|
+
checked = 1
|
|
417
|
+
if not _host_check(path, raw, issues):
|
|
418
|
+
issues.append(FileIssue("check.vba_scope", "warning", path.name, "CFB streams checked; VBA validation is unavailable for this extension"))
|
|
419
|
+
complete = False
|
|
420
|
+
elif path.suffix.lower() in {".accdb", ".mdb"}:
|
|
421
|
+
issues.append(FileIssue("check.access_scope", "warning", path.name, "Access allocation maps and table rows are not covered; use the Access APIs for database inspection and compaction"))
|
|
422
|
+
complete = False
|
|
423
|
+
from pyopenvba.access import AccessDatabase
|
|
424
|
+
with AccessDatabase(raw) as database:
|
|
425
|
+
database.table_names(include_system=True)
|
|
426
|
+
if database.has_vba_project():
|
|
427
|
+
for module in database.vba_project().modules:
|
|
428
|
+
_ = module.source
|
|
429
|
+
checked = 1
|
|
430
|
+
else:
|
|
431
|
+
issues.append(FileIssue("file.format", "error", path.name, "Not a supported Office ZIP, CFB, or Access container"))
|
|
432
|
+
complete = False
|
|
433
|
+
except Exception as exc:
|
|
434
|
+
issues.append(FileIssue("file.parse", "error", path.name, str(exc)))
|
|
435
|
+
complete = False
|
|
436
|
+
return FileCheckReport(path, tuple(issues), checked, complete)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _snapshot(path: Path, max_part_bytes: int, max_total_bytes: int) -> bytes:
|
|
440
|
+
if max_part_bytes <= 0 or max_total_bytes <= 0:
|
|
441
|
+
raise ValueError("Byte limits must be positive")
|
|
442
|
+
with path.open("rb") as stream:
|
|
443
|
+
raw = stream.read(max_total_bytes + 1)
|
|
444
|
+
if len(raw) > max_total_bytes:
|
|
445
|
+
raise ValueError("Input file exceeds max_total_bytes")
|
|
446
|
+
return raw
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def check_file(path: str | Path, *, max_part_bytes: int = _DEFAULT_PART,
|
|
450
|
+
max_total_bytes: int = _DEFAULT_TOTAL) -> FileCheckReport:
|
|
451
|
+
"""Check a disk snapshot without modifying it or executing VBA.
|
|
452
|
+
|
|
453
|
+
ZIP parts are decompressed and CRC checked, XML rejects DTDs, internal
|
|
454
|
+
relationships and content types are checked, and supported VBA projects
|
|
455
|
+
are parsed and validated. Legacy CFB streams are read. Access coverage
|
|
456
|
+
is partial and explicitly reported. Missing files and invalid limits raise;
|
|
457
|
+
malformed contents are returned as diagnostics. Limits can be raised for
|
|
458
|
+
trusted large files; they bound input and uncompressed payload sizes.
|
|
459
|
+
"""
|
|
460
|
+
source = Path(path)
|
|
461
|
+
return _check(_snapshot(source, max_part_bytes, max_total_bytes), source,
|
|
462
|
+
max_part_bytes, max_total_bytes)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def repair_file(path: str | Path, *, output: str | Path,
|
|
466
|
+
max_part_bytes: int = _DEFAULT_PART,
|
|
467
|
+
max_total_bytes: int = _DEFAULT_TOTAL) -> FileRepairResult:
|
|
468
|
+
"""Repair stale ZIP local-header CRC/size fields into a new file.
|
|
469
|
+
|
|
470
|
+
Payloads must pass central-directory CRC verification and all supported
|
|
471
|
+
checks. No parts, VBA, signatures, or compressed bytes are changed. Other
|
|
472
|
+
damage or incomplete checking raises FileRepairError with its report.
|
|
473
|
+
The output must not exist (including the original file); it is written
|
|
474
|
+
only after checking the candidate. A healthy file is copied unchanged.
|
|
475
|
+
This does not rebuild databases or recover missing/corrupt content.
|
|
476
|
+
"""
|
|
477
|
+
source, target = Path(path), Path(output)
|
|
478
|
+
if source.resolve() == target.resolve():
|
|
479
|
+
raise ValueError("Repair output must differ from the original file")
|
|
480
|
+
if target.exists():
|
|
481
|
+
raise FileExistsError(target)
|
|
482
|
+
raw = _snapshot(source, max_part_bytes, max_total_bytes)
|
|
483
|
+
before = _check(raw, source, max_part_bytes, max_total_bytes)
|
|
484
|
+
if not before.complete or any(not issue.repairable for issue in before.errors):
|
|
485
|
+
raise FileRepairError("File has damage or unsupported checks that cannot be safely repaired", before)
|
|
486
|
+
changes = before.errors
|
|
487
|
+
candidate = bytearray(raw)
|
|
488
|
+
if changes:
|
|
489
|
+
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
|
490
|
+
names = {issue.location for issue in changes}
|
|
491
|
+
for info in archive.infolist():
|
|
492
|
+
if info.filename in names:
|
|
493
|
+
struct.pack_into("<III", candidate, info.header_offset + 14,
|
|
494
|
+
info.CRC, info.compress_size, info.file_size)
|
|
495
|
+
after = _check(bytes(candidate), source, max_part_bytes, max_total_bytes)
|
|
496
|
+
if not after.ok:
|
|
497
|
+
raise FileRepairError("Candidate failed verification; no output written", after)
|
|
498
|
+
stream = target.open("xb")
|
|
499
|
+
try:
|
|
500
|
+
with stream:
|
|
501
|
+
stream.write(candidate)
|
|
502
|
+
except BaseException:
|
|
503
|
+
target.unlink(missing_ok=True)
|
|
504
|
+
raise
|
|
505
|
+
after = FileCheckReport(target, after.issues, after.checked_parts, after.complete)
|
|
506
|
+
return FileRepairResult(target, before, after, changes)
|
|
File without changes
|
|
File without changes
|