unmasker 0.1.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.
- unmasker-0.1.0/.editorconfig +17 -0
- unmasker-0.1.0/.github/workflows/ci.yml +94 -0
- unmasker-0.1.0/.github/workflows/release.yml +70 -0
- unmasker-0.1.0/.gitignore +32 -0
- unmasker-0.1.0/.pre-commit-config.yaml +22 -0
- unmasker-0.1.0/CHANGELOG.md +45 -0
- unmasker-0.1.0/CONTRIBUTING.md +183 -0
- unmasker-0.1.0/LICENSE +133 -0
- unmasker-0.1.0/PKG-INFO +544 -0
- unmasker-0.1.0/README.md +510 -0
- unmasker-0.1.0/SECURITY.md +66 -0
- unmasker-0.1.0/assets/unmasker-banner.svg +36 -0
- unmasker-0.1.0/pyproject.toml +83 -0
- unmasker-0.1.0/src/unmasker/__init__.py +11 -0
- unmasker-0.1.0/src/unmasker/about.py +200 -0
- unmasker-0.1.0/src/unmasker/cli.py +220 -0
- unmasker-0.1.0/src/unmasker/detect.py +93 -0
- unmasker-0.1.0/src/unmasker/findings.py +136 -0
- unmasker-0.1.0/src/unmasker/html.py +346 -0
- unmasker-0.1.0/src/unmasker/jpeg.py +219 -0
- unmasker-0.1.0/src/unmasker/markdown.py +247 -0
- unmasker-0.1.0/src/unmasker/metadata/__init__.py +338 -0
- unmasker-0.1.0/src/unmasker/metadata/detectors.py +304 -0
- unmasker-0.1.0/src/unmasker/metadata/xmp.py +292 -0
- unmasker-0.1.0/src/unmasker/odf/__init__.py +1 -0
- unmasker-0.1.0/src/unmasker/odf/revisions.py +159 -0
- unmasker-0.1.0/src/unmasker/odf/sheets.py +323 -0
- unmasker-0.1.0/src/unmasker/odf/slides.py +105 -0
- unmasker-0.1.0/src/unmasker/ooxml/__init__.py +1 -0
- unmasker-0.1.0/src/unmasker/ooxml/revisions.py +169 -0
- unmasker-0.1.0/src/unmasker/ooxml/sheets.py +496 -0
- unmasker-0.1.0/src/unmasker/ooxml/slides.py +150 -0
- unmasker-0.1.0/src/unmasker/pdf/__init__.py +1 -0
- unmasker-0.1.0/src/unmasker/pdf/annotations.py +114 -0
- unmasker-0.1.0/src/unmasker/pdf/detectors.py +863 -0
- unmasker-0.1.0/src/unmasker/pdf/fonts.py +269 -0
- unmasker-0.1.0/src/unmasker/pdf/geometry.py +189 -0
- unmasker-0.1.0/src/unmasker/pdf/interpreter.py +853 -0
- unmasker-0.1.0/src/unmasker/pdf/rendered.py +177 -0
- unmasker-0.1.0/src/unmasker/pdf/tokens.py +344 -0
- unmasker-0.1.0/src/unmasker/py.typed +0 -0
- unmasker-0.1.0/src/unmasker/readers/__init__.py +73 -0
- unmasker-0.1.0/src/unmasker/readers/docx.py +103 -0
- unmasker-0.1.0/src/unmasker/readers/image.py +46 -0
- unmasker-0.1.0/src/unmasker/readers/model.py +112 -0
- unmasker-0.1.0/src/unmasker/readers/odf.py +126 -0
- unmasker-0.1.0/src/unmasker/readers/pdf.py +157 -0
- unmasker-0.1.0/src/unmasker/readers/plain.py +45 -0
- unmasker-0.1.0/src/unmasker/readers/presentation.py +87 -0
- unmasker-0.1.0/src/unmasker/readers/spreadsheet.py +166 -0
- unmasker-0.1.0/src/unmasker/report.py +242 -0
- unmasker-0.1.0/src/unmasker/revisions.py +193 -0
- unmasker-0.1.0/src/unmasker/scan.py +165 -0
- unmasker-0.1.0/src/unmasker/sheets.py +346 -0
- unmasker-0.1.0/src/unmasker/slides.py +133 -0
- unmasker-0.1.0/src/unmasker/survey_report.py +215 -0
- unmasker-0.1.0/src/unmasker/text/__init__.py +1 -0
- unmasker-0.1.0/src/unmasker/text/invisible.py +551 -0
- unmasker-0.1.0/src/unmasker/theme.py +117 -0
- unmasker-0.1.0/src/unmasker/thumbnails.py +174 -0
- unmasker-0.1.0/tests/conftest.py +138 -0
- unmasker-0.1.0/tests/specimens/README.md +165 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-hidden-characters.docx +0 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-hidden-characters.md +65 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-metadata-leak.docx +0 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-metadata-leak.md +73 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-tracked-changes.docx +0 -0
- unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-tracked-changes.md +88 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-no-thumbnail.jpg +0 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-no-thumbnail.md +39 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-stale-thumbnail.jpg +0 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-stale-thumbnail.md +85 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-thumbnail-regenerated.jpg +0 -0
- unmasker-0.1.0/tests/specimens/jpeg/imagemagick-thumbnail-regenerated.md +40 -0
- unmasker-0.1.0/tests/specimens/odf/libreoffice-writer-position-note.md +86 -0
- unmasker-0.1.0/tests/specimens/odf/libreoffice-writer-position-note.odt +0 -0
- unmasker-0.1.0/tests/specimens/odp/libreoffice-impress-hidden-slide.md +53 -0
- unmasker-0.1.0/tests/specimens/odp/libreoffice-impress-hidden-slide.odp +0 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-filtered-rows.md +70 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-filtered-rows.ods +0 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-formatted-values.md +59 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-formatted-values.ods +0 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-hidden-columns.md +90 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-hidden-columns.ods +0 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-tracked-changes.md +98 -0
- unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-tracked-changes.ods +0 -0
- unmasker-0.1.0/tests/specimens/pdf/chrome-print-css-overlay.md +89 -0
- unmasker-0.1.0/tests/specimens/pdf/chrome-print-css-overlay.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/chrome-transparent-text.md +74 -0
- unmasker-0.1.0/tests/specimens/pdf/chrome-transparent-text.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/coverage-edge.md +71 -0
- unmasker-0.1.0/tests/specimens/pdf/coverage-edge.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/flattened-to-image.md +54 -0
- unmasker-0.1.0/tests/specimens/pdf/flattened-to-image.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-clipped-overflow.md +88 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-clipped-overflow.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-rotated-headers.md +95 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-rotated-headers.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-black-bars.md +87 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-black-bars.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-hidden-in-plain-sight.md +78 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-hidden-in-plain-sight.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-image-over-text.md +68 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-image-over-text.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-metadata-leak.md +45 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-metadata-leak.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-partial-bars.md +79 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-partial-bars.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-pdf-comments.md +77 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-pdf-comments.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-properly-redacted.md +65 -0
- unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-properly-redacted.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/redacted-scan-with-ocr.md +82 -0
- unmasker-0.1.0/tests/specimens/pdf/redacted-scan-with-ocr.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/text-on-an-image.md +73 -0
- unmasker-0.1.0/tests/specimens/pdf/text-on-an-image.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pdf/xmp-survives-the-scrub.md +108 -0
- unmasker-0.1.0/tests/specimens/pdf/xmp-survives-the-scrub.pdf +0 -0
- unmasker-0.1.0/tests/specimens/pptx/libreoffice-impress-hidden-slide.md +87 -0
- unmasker-0.1.0/tests/specimens/pptx/libreoffice-impress-hidden-slide.pptx +0 -0
- unmasker-0.1.0/tests/specimens/sources/build_calc_filtered_rows.py +119 -0
- unmasker-0.1.0/tests/specimens/sources/build_calc_formatted_values.py +134 -0
- unmasker-0.1.0/tests/specimens/sources/build_calc_hidden_columns.py +192 -0
- unmasker-0.1.0/tests/specimens/sources/build_calc_tracked_changes.py +186 -0
- unmasker-0.1.0/tests/specimens/sources/build_chrome_print.py +121 -0
- unmasker-0.1.0/tests/specimens/sources/build_chrome_transparent_text.py +96 -0
- unmasker-0.1.0/tests/specimens/sources/build_clipped_overflow.py +115 -0
- unmasker-0.1.0/tests/specimens/sources/build_coverage_edge.py +191 -0
- unmasker-0.1.0/tests/specimens/sources/build_docx_hidden_characters.py +121 -0
- unmasker-0.1.0/tests/specimens/sources/build_docx_tracked_changes.py +152 -0
- unmasker-0.1.0/tests/specimens/sources/build_image_over_text.py +213 -0
- unmasker-0.1.0/tests/specimens/sources/build_impress_deck.py +163 -0
- unmasker-0.1.0/tests/specimens/sources/build_libreoffice_hidden_in_plain_sight.py +268 -0
- unmasker-0.1.0/tests/specimens/sources/build_libreoffice_writer.py +408 -0
- unmasker-0.1.0/tests/specimens/sources/build_metadata_leak.py +128 -0
- unmasker-0.1.0/tests/specimens/sources/build_odf_document.py +166 -0
- unmasker-0.1.0/tests/specimens/sources/build_pdf_with_comments.py +125 -0
- unmasker-0.1.0/tests/specimens/sources/build_redacted_scan_with_ocr.py +193 -0
- unmasker-0.1.0/tests/specimens/sources/build_rotated_text.py +139 -0
- unmasker-0.1.0/tests/specimens/sources/build_stale_thumbnail.py +160 -0
- unmasker-0.1.0/tests/specimens/sources/build_text_on_an_image.py +202 -0
- unmasker-0.1.0/tests/specimens/sources/build_xmp_survives_the_scrub.py +151 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-formatted-values.md +90 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-formatted-values.xlsx +0 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-hidden-columns.md +93 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-hidden-columns.xlsx +0 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-tracked-changes.md +74 -0
- unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-tracked-changes.xlsx +0 -0
- unmasker-0.1.0/tests/test_about.py +206 -0
- unmasker-0.1.0/tests/test_cli.py +446 -0
- unmasker-0.1.0/tests/test_documented_detectors.py +216 -0
- unmasker-0.1.0/tests/test_html.py +256 -0
- unmasker-0.1.0/tests/test_invisible.py +215 -0
- unmasker-0.1.0/tests/test_markdown.py +265 -0
- unmasker-0.1.0/tests/test_metadata.py +321 -0
- unmasker-0.1.0/tests/test_ocr.py +379 -0
- unmasker-0.1.0/tests/test_odf.py +226 -0
- unmasker-0.1.0/tests/test_ooxml_revisions.py +250 -0
- unmasker-0.1.0/tests/test_pdf_annotations.py +175 -0
- unmasker-0.1.0/tests/test_pdf_detectors.py +1158 -0
- unmasker-0.1.0/tests/test_pdf_fonts.py +163 -0
- unmasker-0.1.0/tests/test_pdf_geometry.py +140 -0
- unmasker-0.1.0/tests/test_pdf_interpreter.py +323 -0
- unmasker-0.1.0/tests/test_pdf_text.py +310 -0
- unmasker-0.1.0/tests/test_pdf_tokens.py +245 -0
- unmasker-0.1.0/tests/test_readers.py +251 -0
- unmasker-0.1.0/tests/test_scan.py +155 -0
- unmasker-0.1.0/tests/test_sheet_changes.py +214 -0
- unmasker-0.1.0/tests/test_sheets.py +759 -0
- unmasker-0.1.0/tests/test_slides.py +329 -0
- unmasker-0.1.0/tests/test_survey_report.py +233 -0
- unmasker-0.1.0/tests/test_thumbnails.py +300 -0
- unmasker-0.1.0/tests/test_xmp.py +340 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
indent_style = space
|
|
8
|
+
indent_size = 4
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.{md,yml,yaml,json,toml,svg}]
|
|
12
|
+
indent_size = 2
|
|
13
|
+
|
|
14
|
+
# Specimens are artefacts under test. Nothing may normalise their bytes.
|
|
15
|
+
[tests/specimens/**]
|
|
16
|
+
insert_final_newline = false
|
|
17
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
17
|
+
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install
|
|
26
|
+
run: python -m pip install -e ".[dev]"
|
|
27
|
+
# One test shells out to poppler, and it is the one that proves this tool
|
|
28
|
+
# says something the obvious answer does not: `pdftotext` reports no
|
|
29
|
+
# annotations, so a comment carrying "Do not minute this" is invisible to
|
|
30
|
+
# the first thing anybody reaches for. It skips where poppler is absent,
|
|
31
|
+
# so installing it on one platform is the difference between that claim
|
|
32
|
+
# being checked somewhere and checked nowhere.
|
|
33
|
+
- name: Poppler (for the one test that needs it)
|
|
34
|
+
if: matrix.os == 'ubuntu-latest'
|
|
35
|
+
run: sudo apt-get update && sudo apt-get install -y poppler-utils
|
|
36
|
+
# `ruff check`, not `ruff format --check`. This project lints and does
|
|
37
|
+
# not auto-format: the layout of the report, the specimens' XML and the
|
|
38
|
+
# docstrings are placed deliberately, and a formatter run at publication
|
|
39
|
+
# time would rewrite twelve files nobody asked it to. CI asserts the
|
|
40
|
+
# standard the project actually holds, which is the one in README.md.
|
|
41
|
+
- name: Ruff
|
|
42
|
+
run: ruff check .
|
|
43
|
+
# The specimens are the test suite here and they are committed, so this
|
|
44
|
+
# runs the real thing on every platform. `--ocr` is not exercised: it
|
|
45
|
+
# needs ghostscript and tesseract, and `tests/test_ocr.py` skips rather
|
|
46
|
+
# than failing over their absence. Every test that shells out is guarded
|
|
47
|
+
# the same way - the first CI run failed because one of them was not, and
|
|
48
|
+
# nothing on this machine could have told us.
|
|
49
|
+
- name: Tests
|
|
50
|
+
run: pytest
|
|
51
|
+
|
|
52
|
+
types:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- uses: actions/setup-python@v5
|
|
58
|
+
with:
|
|
59
|
+
python-version: "3.12"
|
|
60
|
+
- name: Install
|
|
61
|
+
run: python -m pip install -e ".[dev]"
|
|
62
|
+
- name: Mypy
|
|
63
|
+
run: mypy
|
|
64
|
+
|
|
65
|
+
package:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
|
|
68
|
+
# The tests above run from a checkout, so they prove nothing about the
|
|
69
|
+
# artifact people actually install. A wheel that cannot be built, or that
|
|
70
|
+
# is missing a file, fails only for them and only after publication.
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
- uses: actions/setup-python@v5
|
|
74
|
+
with:
|
|
75
|
+
python-version: "3.12"
|
|
76
|
+
- name: Build
|
|
77
|
+
run: |
|
|
78
|
+
python -m pip install build twine
|
|
79
|
+
python -m build
|
|
80
|
+
twine check dist/*
|
|
81
|
+
- name: The typing marker has to be inside the wheel
|
|
82
|
+
run: |
|
|
83
|
+
python - <<'CHECK'
|
|
84
|
+
import glob, zipfile
|
|
85
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
86
|
+
assert "unmasker/py.typed" in names, names
|
|
87
|
+
CHECK
|
|
88
|
+
# Installing it and running it is the part that catches an import which
|
|
89
|
+
# type-checks and does not exist at runtime. Mypy passing is not the
|
|
90
|
+
# program running.
|
|
91
|
+
- name: Install what was built and run it
|
|
92
|
+
run: |
|
|
93
|
+
python -m pip install dist/*.whl
|
|
94
|
+
unmasker --version
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
|
|
15
|
+
# Trusted Publishing: PyPI verifies this workflow's identity over OIDC, so
|
|
16
|
+
# there is no API token to store, rotate or leak. `id-token: write` is what
|
|
17
|
+
# mints that identity and is the only permission this needs.
|
|
18
|
+
permissions:
|
|
19
|
+
id-token: write
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
|
|
27
|
+
# A tag is a claim about which version is being released, and nothing
|
|
28
|
+
# else checks it. Tagging v0.2.0 against a pyproject that still says
|
|
29
|
+
# 0.1.0 publishes 0.1.0 under a tag that says otherwise, and PyPI will
|
|
30
|
+
# not let that file name be reused once it is wrong.
|
|
31
|
+
- name: The tag has to be the version being built
|
|
32
|
+
run: |
|
|
33
|
+
python - <<'CHECK'
|
|
34
|
+
import os, pathlib, re, sys
|
|
35
|
+
|
|
36
|
+
tag = os.environ["GITHUB_REF_NAME"]
|
|
37
|
+
text = pathlib.Path("pyproject.toml").read_text(encoding="utf-8")
|
|
38
|
+
declared = re.search(r'^version = "([^"]+)"', text, re.MULTILINE).group(1)
|
|
39
|
+
|
|
40
|
+
if tag != f"v{declared}":
|
|
41
|
+
sys.exit(f"tag {tag} does not match pyproject version {declared}")
|
|
42
|
+
print(f"{tag} matches pyproject {declared}")
|
|
43
|
+
CHECK
|
|
44
|
+
|
|
45
|
+
- name: Build
|
|
46
|
+
run: |
|
|
47
|
+
python -m pip install build twine
|
|
48
|
+
python -m build
|
|
49
|
+
twine check dist/*
|
|
50
|
+
|
|
51
|
+
# The same assertion CI makes on every push. It is repeated here because
|
|
52
|
+
# this is the artifact that actually reaches people, and a wheel missing
|
|
53
|
+
# its typing marker is invisible until somebody imports the package.
|
|
54
|
+
- name: The typing marker has to be inside the wheel
|
|
55
|
+
run: |
|
|
56
|
+
python - <<'CHECK'
|
|
57
|
+
import glob, zipfile
|
|
58
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
59
|
+
assert "unmasker/py.typed" in names, names
|
|
60
|
+
CHECK
|
|
61
|
+
|
|
62
|
+
# Install the built wheel in a clean interpreter and run the console
|
|
63
|
+
# script. Mypy passing is not the program running, and neither is a green
|
|
64
|
+
# test suite executed from a checkout.
|
|
65
|
+
- name: Install what was built and run it
|
|
66
|
+
run: |
|
|
67
|
+
python -m pip install dist/*.whl
|
|
68
|
+
unmasker --version
|
|
69
|
+
|
|
70
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.vscode/
|
|
11
|
+
.idea/
|
|
12
|
+
.DS_Store
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Review archives built with `git archive`: a snapshot of the repository stored
|
|
17
|
+
# inside that repository is redundant the day it is made and wrong after it.
|
|
18
|
+
*.zip
|
|
19
|
+
|
|
20
|
+
# Specimens are the test suite here and DO belong in git - see CLAUDE.md.
|
|
21
|
+
# Only genuinely private material is excluded.
|
|
22
|
+
private-samples/
|
|
23
|
+
|
|
24
|
+
# Working notes, not documentation, and the only two files here that are not
|
|
25
|
+
# published. HANDOFF.md is the running log of what was tried and what it cost,
|
|
26
|
+
# and it names files in a sibling checkout that are private. CLAUDE.md is how
|
|
27
|
+
# to work in this repository rather than what the tool does.
|
|
28
|
+
#
|
|
29
|
+
# CONTRIBUTING.md is tracked, deliberately: its rules are each named after the
|
|
30
|
+
# failure that produced them, and that is worth publishing.
|
|
31
|
+
HANDOFF.md
|
|
32
|
+
CLAUDE.md
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Lint only. This project does not auto-format: the report layout, the
|
|
2
|
+
# specimens' XML and the docstrings are placed deliberately, and `ruff format`
|
|
3
|
+
# would rewrite a dozen files nobody asked it to. CI runs the same check.
|
|
4
|
+
repos:
|
|
5
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
6
|
+
rev: v0.6.9
|
|
7
|
+
hooks:
|
|
8
|
+
- id: ruff
|
|
9
|
+
args: [--fix]
|
|
10
|
+
|
|
11
|
+
# `if False:` is how a watched failure gets parked while the test that proves
|
|
12
|
+
# it is written, and ruff's selected rules (E, F, I, UP, B) do not flag it.
|
|
13
|
+
# One such guard reached the working tree in `jpeg.py` and disabled the
|
|
14
|
+
# start-of-scan check, so `dimensions()` read a frame header out of
|
|
15
|
+
# compressed data. The test caught it; nothing else would have.
|
|
16
|
+
- repo: local
|
|
17
|
+
hooks:
|
|
18
|
+
- id: no-parked-guards
|
|
19
|
+
name: no `if False:` left in src/
|
|
20
|
+
entry: 'if False:'
|
|
21
|
+
language: pygrep
|
|
22
|
+
files: ^src/.*\.py$
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to `unmasker`, newest first. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
`--json` carries its own `schema` field, which is versioned separately from the
|
|
8
|
+
release: `unmasker.scan/1` for one file, `unmasker.survey/1` for a folder. A
|
|
9
|
+
consumer should read that rather than the release number, because the release
|
|
10
|
+
moves whenever anything does and the schema moves only when the shape changes.
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-09-05
|
|
13
|
+
|
|
14
|
+
First release.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- 25 detectors across seven groups: covered and under-image text, invisible and
|
|
19
|
+
low-contrast text, off-page text; zero-width, bidi, tag and mixed-script
|
|
20
|
+
characters; hidden sheets, rows, columns, filtered rows and tracked cell
|
|
21
|
+
changes; hidden slides and speaker notes; stale EXIF thumbnails; tracked
|
|
22
|
+
deletions, comments, revision history and metadata leaks; and — with `--ocr` —
|
|
23
|
+
text that renders but does not extract, and text that extracts but does not
|
|
24
|
+
render.
|
|
25
|
+
- Readers for PDF, DOCX, ODT, XLSX, ODS, PPTX, ODP, JPEG and plain text,
|
|
26
|
+
dispatched by content rather than by extension.
|
|
27
|
+
- Folder surveys: point it at a directory and it reports which files to open
|
|
28
|
+
next, without ranking them against each other.
|
|
29
|
+
- `--html` and `--md` reports, both escaping every value they quote, because
|
|
30
|
+
everything quoted came out of a document somebody else wrote.
|
|
31
|
+
- `--json` for pipelines, with a `schema` field naming the shape and a
|
|
32
|
+
`searched` field distinguishing *nothing was found* from *there was nothing to
|
|
33
|
+
search*.
|
|
34
|
+
- Three exit statuses: `0` clean, `1` findings, `2` unreadable — because a file
|
|
35
|
+
that could not be read is not a file that came back clean.
|
|
36
|
+
- `py.typed`, so the annotations are visible to anything importing `unmasker`
|
|
37
|
+
as a library.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `jpeg.dimensions()` walked past the start-of-scan marker and read a `0xFFC0`
|
|
42
|
+
out of the entropy-coded data, reporting a picture the size of noise. It now
|
|
43
|
+
stops at `SOS`/`EOI` the way `_segments()` already did.
|
|
44
|
+
|
|
45
|
+
[0.1.0]: https://github.com/osint-shifu/unmasker/releases/tag/v0.1.0
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for looking. This project has a small number of rules and every one
|
|
4
|
+
of them was paid for by a bug. They are worth reading before writing code,
|
|
5
|
+
because a change that breaks one of them will be asked to change back.
|
|
6
|
+
|
|
7
|
+
## Setting up
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python3 -m venv .venv
|
|
11
|
+
.venv/bin/pip install -e ".[dev]"
|
|
12
|
+
.venv/bin/pytest
|
|
13
|
+
.venv/bin/ruff check .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Python 3.10 or later. One runtime dependency, `pypdf`.
|
|
17
|
+
|
|
18
|
+
## The one thing this tool must never do
|
|
19
|
+
|
|
20
|
+
**State something the evidence does not support.** No "this document was
|
|
21
|
+
manipulated". Show what a human sees, show what a machine reads, name the gap,
|
|
22
|
+
and stop.
|
|
23
|
+
|
|
24
|
+
A reader who disagrees with a finding must be able to see why. A tool that
|
|
25
|
+
concludes for its reader is a tool whose conclusion has to be trusted blindly,
|
|
26
|
+
and nobody should trust this one blindly.
|
|
27
|
+
|
|
28
|
+
Everything below follows from that.
|
|
29
|
+
|
|
30
|
+
## A new detector needs a specimen a real producer wrote
|
|
31
|
+
|
|
32
|
+
This is the rule that matters most, and it is not negotiable.
|
|
33
|
+
|
|
34
|
+
A fixture built from a specification proves that your reader agrees with your
|
|
35
|
+
reading of the spec. It does not prove it agrees with LibreOffice. The sibling
|
|
36
|
+
project `filetrail` had a HEIC reader with a fully green test suite that
|
|
37
|
+
decoded nothing at all on every real file, because every fixture had been built
|
|
38
|
+
from the standard and no real encoder writes files that way.
|
|
39
|
+
|
|
40
|
+
So:
|
|
41
|
+
|
|
42
|
+
1. Write a **builder** in `tests/specimens/sources/` that drives a real
|
|
43
|
+
producer — `soffice`, `google-chrome`, `gs`, `tesseract`, `exiftool` — to
|
|
44
|
+
write the file.
|
|
45
|
+
2. Commit the produced file. The specimens **are** the test suite here, which
|
|
46
|
+
is why they are in git rather than gitignored.
|
|
47
|
+
3. Write a `.md` beside it recording what produced it, what a human sees, what
|
|
48
|
+
is actually in the file, and **what it does not carry**.
|
|
49
|
+
|
|
50
|
+
Everything in a specimen must be invented. No real documents, no real people;
|
|
51
|
+
the e-mail domain is `example.org`, which RFC 2606 reserves for the purpose.
|
|
52
|
+
|
|
53
|
+
If the producer does something the specification does not describe — and it
|
|
54
|
+
will — that is the most valuable thing in your pull request. Write it down.
|
|
55
|
+
|
|
56
|
+
## Watch the test fail first
|
|
57
|
+
|
|
58
|
+
Write the test, run it, read the failure, *then* implement. A test written
|
|
59
|
+
afterwards passes against broken code, and this project has caught itself
|
|
60
|
+
doing that twice.
|
|
61
|
+
|
|
62
|
+
Check the failure is the one you expected. An `ImportError` is not evidence
|
|
63
|
+
that your test works.
|
|
64
|
+
|
|
65
|
+
## Break your own rule and see whether the suite notices
|
|
66
|
+
|
|
67
|
+
Before opening a pull request, take each claim your docstrings make, break it
|
|
68
|
+
in the source, and run the suite. Anything that stays green is a claim nothing
|
|
69
|
+
is holding.
|
|
70
|
+
|
|
71
|
+
This has found something every single time it has been run here — most
|
|
72
|
+
recently a guard that could not fail because the fixture feeding it had a
|
|
73
|
+
default that made the question moot. If you find one, the fix is a test, and
|
|
74
|
+
that test is the interesting part of the change.
|
|
75
|
+
|
|
76
|
+
## Report a line, never a show-operation
|
|
77
|
+
|
|
78
|
+
If you are touching the PDF detectors: producers disagree wildly about how much
|
|
79
|
+
text one drawing operation carries. Chrome writes one glyph per `Tj`; tesseract
|
|
80
|
+
writes one operation per word. Group by **line on the page**, using
|
|
81
|
+
`detectors._lines`, or a single hidden line becomes eighty-seven findings.
|
|
82
|
+
|
|
83
|
+
That rule has been broken five times in this codebase.
|
|
84
|
+
`test_no_detector_reports_per_show_operation` now asks the question directly.
|
|
85
|
+
|
|
86
|
+
## Do not rank findings against each other
|
|
87
|
+
|
|
88
|
+
A page can have a bar over its text *and* invisible characters *and* stale
|
|
89
|
+
metadata. Those are three findings, not one winner. `filetrail` printed the
|
|
90
|
+
winner once, and a geotagged photograph that had been downloaded reported its
|
|
91
|
+
URL and no GPS at all.
|
|
92
|
+
|
|
93
|
+
Nothing here sorts by strength, and `Finding` carries no score.
|
|
94
|
+
|
|
95
|
+
## "Nothing found" has two meanings
|
|
96
|
+
|
|
97
|
+
It can mean *searched, and it is not there*. It can mean *there was nothing to
|
|
98
|
+
search*. A reader who confuses those has drawn a conclusion the tool never
|
|
99
|
+
supported.
|
|
100
|
+
|
|
101
|
+
Carry the difference in the data rather than reconstructing it later — a PDF
|
|
102
|
+
with no content stream to parse is not a PDF with nothing hidden, and the
|
|
103
|
+
report has to be able to say which it met.
|
|
104
|
+
|
|
105
|
+
## The name of a field is evidence
|
|
106
|
+
|
|
107
|
+
`LibreOffice/24.2.7.2$Linux_X86_64` contains a dotted quad. Pattern-matching
|
|
108
|
+
alone reports an IP address — but the field is called `Producer`, so it is a
|
|
109
|
+
version. Every metadata field's meaning is decided by its name **and its
|
|
110
|
+
container**: a PDF's `/Creator` is an application, an OOXML `dc:creator` is a
|
|
111
|
+
person.
|
|
112
|
+
|
|
113
|
+
Context you already have beats a cleverer pattern.
|
|
114
|
+
|
|
115
|
+
## Colour and words
|
|
116
|
+
|
|
117
|
+
**Colour encodes how the tool knows, never how bad the finding is.** Three
|
|
118
|
+
classes — `direct`, `circumstantial`, `self-reported` — and nothing else in
|
|
119
|
+
the output may use colour.
|
|
120
|
+
|
|
121
|
+
Prefer a word a reader can argue with over a number that implies a precision
|
|
122
|
+
you do not have. `55` reads as a probability; it never was one.
|
|
123
|
+
|
|
124
|
+
## Modes are commands, not flags
|
|
125
|
+
|
|
126
|
+
If an option ignores most of the other options and is mutually exclusive with
|
|
127
|
+
its neighbours, it is a subcommand. `--json` changes the shape of the output,
|
|
128
|
+
not what the tool did, which is what keeps it a flag.
|
|
129
|
+
|
|
130
|
+
## Output
|
|
131
|
+
|
|
132
|
+
- **Nothing is truncated.** A value too long for the line wraps. An ellipsis
|
|
133
|
+
sends the reader to fetch the value another way.
|
|
134
|
+
- **Nothing draws past the width it declared.**
|
|
135
|
+
- **Size a column per section, not once per screen.** One global width makes a
|
|
136
|
+
layout look *almost* aligned, which reads worse than not aligning it.
|
|
137
|
+
- A description that restates its flag teaches the reader to skip
|
|
138
|
+
descriptions, and once they skip one they skip the rest.
|
|
139
|
+
- Every command the tool prints must run in the shell that printed it. A screen
|
|
140
|
+
disproved by the first thing a reader tries loses credibility for everything
|
|
141
|
+
else on it.
|
|
142
|
+
|
|
143
|
+
The report's layout and palette are the sibling project `filetrail`'s, and its
|
|
144
|
+
`DESIGN.md` is the design language for both — **read it before inventing a
|
|
145
|
+
second one.** The same goes for anything it already solves: its embedded
|
|
146
|
+
metadata sources read PDF, OOXML, ODF, legacy OLE2 and EXIF, and were read
|
|
147
|
+
before the metadata reader here was written.
|
|
148
|
+
|
|
149
|
+
What is deliberately *not* shared is the identity. `filetrail` is a terminal;
|
|
150
|
+
`unmasker` is the document the terminal is looking at.
|
|
151
|
+
|
|
152
|
+
## Commits
|
|
153
|
+
|
|
154
|
+
Prose, and no trailers. Say what changed and **why** — the why is the part
|
|
155
|
+
that cannot be recovered from the diff. If a change corrects an earlier
|
|
156
|
+
assumption, say what the assumption was; those are the most useful messages in
|
|
157
|
+
this repository's history.
|
|
158
|
+
|
|
159
|
+
Stage deliberately. `git add -A` once swept a build archive into two commits of
|
|
160
|
+
the sibling project and it reached a public repository.
|
|
161
|
+
|
|
162
|
+
## What a good pull request looks like
|
|
163
|
+
|
|
164
|
+
- a specimen a producer wrote, with its `.md`
|
|
165
|
+
- a test you watched fail
|
|
166
|
+
- the producer fact you measured, written down where the next person will hit it
|
|
167
|
+
- the gap your change does *not* close, named rather than left implied
|
|
168
|
+
|
|
169
|
+
## Reporting a bug
|
|
170
|
+
|
|
171
|
+
The most useful report is a file. If it is not one you can share — and in this
|
|
172
|
+
line of work it usually is not — a builder that produces something with the
|
|
173
|
+
same shape is just as good, and better for everyone.
|
|
174
|
+
|
|
175
|
+
If a finding is wrong, say what the tool printed and what the document actually
|
|
176
|
+
shows. A false positive is a bug of the same size as a miss here: a report a
|
|
177
|
+
reader learns to scroll past has stopped working.
|
|
178
|
+
|
|
179
|
+
## Security
|
|
180
|
+
|
|
181
|
+
See [`SECURITY.md`](SECURITY.md). In short: this tool parses hostile files by
|
|
182
|
+
design, so a crash or a hang on a malformed document is a bug worth reporting
|
|
183
|
+
privately first.
|
unmasker-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and
|
|
10
|
+
distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by the
|
|
13
|
+
copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all other
|
|
16
|
+
entities that control, are controlled by, or are under common control with
|
|
17
|
+
that entity. For the purposes of this definition, "control" means (i) the
|
|
18
|
+
power, direct or indirect, to cause the direction or management of such
|
|
19
|
+
entity, whether by contract or otherwise, or (ii) ownership of fifty percent
|
|
20
|
+
(50%) or more of the outstanding shares, or (iii) beneficial ownership of
|
|
21
|
+
such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
|
24
|
+
permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation source,
|
|
28
|
+
and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical transformation
|
|
31
|
+
or translation of a Source form, including but not limited to compiled object
|
|
32
|
+
code, generated documentation, and conversions to other media types.
|
|
33
|
+
|
|
34
|
+
"Work" shall mean the work of authorship, whether in Source or Object form,
|
|
35
|
+
made available under the License, as indicated by a copyright notice that is
|
|
36
|
+
included in or attached to the work.
|
|
37
|
+
|
|
38
|
+
"Derivative Works" shall mean any work, whether in Source or Object form,
|
|
39
|
+
that is based on (or derived from) the Work and for which the editorial
|
|
40
|
+
revisions, annotations, elaborations, or other modifications represent, as a
|
|
41
|
+
whole, an original work of authorship. For the purposes of this License,
|
|
42
|
+
Derivative Works shall not include works that remain separable from, or
|
|
43
|
+
merely link (or bind by name) to the interfaces of, the Work and Derivative
|
|
44
|
+
Works thereof.
|
|
45
|
+
|
|
46
|
+
"Contribution" shall mean any work of authorship, including the original
|
|
47
|
+
version of the Work and any modifications or additions to that Work or
|
|
48
|
+
Derivative Works thereof, that is intentionally submitted to Licensor for
|
|
49
|
+
inclusion in the Work by the copyright owner or by an individual or Legal
|
|
50
|
+
Entity authorized to submit on behalf of the copyright owner.
|
|
51
|
+
|
|
52
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on
|
|
53
|
+
behalf of whom a Contribution has been received by Licensor and subsequently
|
|
54
|
+
incorporated within the Work.
|
|
55
|
+
|
|
56
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this
|
|
57
|
+
License, each Contributor hereby grants to You a perpetual, worldwide,
|
|
58
|
+
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
|
|
59
|
+
reproduce, prepare Derivative Works of, publicly display, publicly perform,
|
|
60
|
+
sublicense, and distribute the Work and such Derivative Works in Source or
|
|
61
|
+
Object form.
|
|
62
|
+
|
|
63
|
+
3. Grant of Patent License. Subject to the terms and conditions of this
|
|
64
|
+
License, each Contributor hereby grants to You a perpetual, worldwide,
|
|
65
|
+
non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
|
|
66
|
+
section) patent license to make, have made, use, offer to sell, sell, import,
|
|
67
|
+
and otherwise transfer the Work, where such license applies only to those
|
|
68
|
+
patent claims licensable by such Contributor that are necessarily infringed
|
|
69
|
+
by their Contribution(s) alone or by combination of their Contribution(s)
|
|
70
|
+
with the Work to which such Contribution(s) was submitted.
|
|
71
|
+
|
|
72
|
+
If You institute patent litigation against any entity (including a
|
|
73
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
|
74
|
+
Contribution incorporated within the Work constitutes direct or contributory
|
|
75
|
+
patent infringement, then any patent licenses granted to You under this
|
|
76
|
+
License for that Work shall terminate as of the date such litigation is filed.
|
|
77
|
+
|
|
78
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or
|
|
79
|
+
Derivative Works thereof in any medium, with or without modifications, and
|
|
80
|
+
in Source or Object form, provided that You meet the following conditions:
|
|
81
|
+
|
|
82
|
+
(a) You must give any other recipients of the Work or Derivative Works a
|
|
83
|
+
copy of this License; and
|
|
84
|
+
|
|
85
|
+
(b) You must cause any modified files to carry prominent notices stating
|
|
86
|
+
that You changed the files; and
|
|
87
|
+
|
|
88
|
+
(c) You must retain, in the Source form of any Derivative Works that You
|
|
89
|
+
distribute, all copyright, patent, trademark, and attribution notices from
|
|
90
|
+
the Source form of the Work, excluding those notices that do not pertain to
|
|
91
|
+
any part of the Derivative Works; and
|
|
92
|
+
|
|
93
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
|
94
|
+
then any Derivative Works that You distribute must include a readable copy
|
|
95
|
+
of the attribution notices contained within such NOTICE file, excluding those
|
|
96
|
+
notices that do not pertain to any part of the Derivative Works, in at least
|
|
97
|
+
one of the following places: within a NOTICE text file distributed as part of
|
|
98
|
+
the Derivative Works; within the Source form or documentation, if provided
|
|
99
|
+
along with the Derivative Works; or, within a display generated by the
|
|
100
|
+
Derivative Works, if and wherever such third-party notices normally appear.
|
|
101
|
+
|
|
102
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
|
103
|
+
Contribution intentionally submitted for inclusion in the Work by You to the
|
|
104
|
+
Licensor shall be under the terms and conditions of this License, without
|
|
105
|
+
any additional terms or conditions.
|
|
106
|
+
|
|
107
|
+
6. Trademarks. This License does not grant permission to use the trade names,
|
|
108
|
+
trademarks, service marks, or product names of the Licensor, except as
|
|
109
|
+
required for reasonable and customary use in describing the origin of the
|
|
110
|
+
Work and reproducing the content of the NOTICE file.
|
|
111
|
+
|
|
112
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
|
|
113
|
+
writing, Licensor provides the Work (and each Contributor provides its
|
|
114
|
+
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
115
|
+
KIND, either express or implied, including, without limitation, any warranties
|
|
116
|
+
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
117
|
+
PARTICULAR PURPOSE.
|
|
118
|
+
|
|
119
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in
|
|
120
|
+
tort (including negligence), contract, or otherwise, unless required by
|
|
121
|
+
applicable law, shall any Contributor be liable to You for damages, including
|
|
122
|
+
any direct, indirect, special, incidental, or consequential damages of any
|
|
123
|
+
character arising as a result of this License or out of the use or inability
|
|
124
|
+
to use the Work.
|
|
125
|
+
|
|
126
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work
|
|
127
|
+
or Derivative Works thereof, You may choose to offer, and charge a fee for,
|
|
128
|
+
acceptance of support, warranty, indemnity, or other liability obligations
|
|
129
|
+
and/or rights consistent with this License. However, in accepting such
|
|
130
|
+
obligations, You may act only on Your own behalf and on Your sole
|
|
131
|
+
responsibility, not on behalf of any other Contributor.
|
|
132
|
+
|
|
133
|
+
END OF TERMS AND CONDITIONS
|