puremagic 2.1.0__tar.gz → 2.2.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.
- {puremagic-2.1.0 → puremagic-2.2.0}/CHANGELOG.md +17 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/PKG-INFO +1 -1
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/main.py +25 -8
- puremagic-2.2.0/puremagic/scanners/asf_scanner.py +59 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/cfbf_scanner.py +52 -3
- puremagic-2.2.0/puremagic/scanners/ebml_scanner.py +21 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/hdf5_scanner.py +1 -1
- puremagic-2.2.0/puremagic/scanners/ogg_scanner.py +39 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/text_scanner.py +4 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/zip_scanner.py +147 -12
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic.egg-info/PKG-INFO +1 -1
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic.egg-info/SOURCES.txt +8 -0
- puremagic-2.2.0/test/resources/audio/test.oga +0 -0
- puremagic-2.2.0/test/resources/audio/test.ogg +0 -0
- puremagic-2.2.0/test/resources/audio/test.wma +0 -0
- puremagic-2.2.0/test/resources/video/test.ogv +0 -0
- puremagic-2.2.0/test/resources/video/test.webm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/test_common_extensions.py +0 -1
- puremagic-2.2.0/test/test_scanners.py +882 -0
- puremagic-2.2.0/uv.lock +882 -0
- puremagic-2.1.0/test/test_scanners.py +0 -118
- puremagic-2.1.0/uv.lock +0 -827
- {puremagic-2.1.0 → puremagic-2.2.0}/.claude/skills/changelog.md +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/.coveragerc +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/.github/workflows/pythonpublish.yml +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/.github/workflows/tests.yml +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/.gitignore +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/.pre-commit-config.yaml +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/AUTHORS.rst +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/LICENSE +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/MANIFEST.in +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/README.rst +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/__init__.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/__main__.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/magic_data.json +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/py.typed +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/__init__.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/helpers.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/json_scanner.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/mpeg_audio_scanner.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/pdf_scanner.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/python_scanner.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic/scanners/sndhdr_scanner.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic.egg-info/dependency_links.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic.egg-info/entry_points.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/puremagic.egg-info/top_level.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/pyproject.toml +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/requirements-test.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/scripts/parse_ftk_kessler_sigs.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/setup.cfg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/__init__.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/common.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.7z +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.big_endian.pcapng +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.bz2 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.cb7 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.cbt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.cbz +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.fb2.zip +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.fbz +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.gz +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.little_endian.pcapng +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.pcapng +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.rar +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.tar +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.txt.lz4 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.txt.zst +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.xz +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/archive/test.zip +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.aac +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.aif +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.flac +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.opus +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.sndr +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test.wav +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_info_128k_notags.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_apev2_tagplus_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_notags.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_apev2_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/fake_file +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.avif +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.bmp +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.gif +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.ico +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.jpg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.png +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.psd +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.svg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.tga +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.tif +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.webp +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test.xcf +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test_varriant.svg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/images/test_varriant_2.svg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/media/test (single).vmdk +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/media/test (split).vmdk +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/media/test.iso +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/media/test.mdf +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/media/test.swf +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.doc +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.docm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.docx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.dotm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.dotx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.eml +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.fb2 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.msg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.odp +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.ods +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.odt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.pdf +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.potm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.potx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.ppt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.pptm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.pptx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xls +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xlsb +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xlsm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xlsx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xltm +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test.xltx +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/test_utf16le.csv +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/text_cr.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/text_crlf.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/office/text_lf.txt +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/system/test.exe +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/system/test.json +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/system/test.puremagic_multi_footer +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/system/test.py +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/system/test_list.json +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.3g2 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.avi +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.flv +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.jxsv +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.mkv +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.mp4 +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.mpg +0 -0
- {puremagic-2.1.0 → puremagic-2.2.0}/test/resources/video/test.wmv +0 -0
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
Version 2.2.0
|
|
5
|
+
-------------
|
|
6
|
+
|
|
7
|
+
- Adding Ogg scanner to distinguish Vorbis, Opus, Theora, FLAC, Speex, and OGM codecs
|
|
8
|
+
- Adding ASF scanner to distinguish WMV (video) from WMA (audio) files
|
|
9
|
+
- Adding EBML scanner to distinguish Matroska (.mkv) from WebM (.webm) files
|
|
10
|
+
- Adding MSI (Windows Installer) and MPP (Microsoft Project) detection to CFBF scanner
|
|
11
|
+
- Fixing #146 OOXML detection now uses `[Content_Types].xml` content types as primary method, correctly identifying docx/xlsx/pptx files from LibreOffice, Google Docs, and other non-Microsoft tools (thanks to jonasdeboeck79)
|
|
12
|
+
- Fixing ZIP deep scan now inspects all ZIP files instead of short-circuiting on .zip extension
|
|
13
|
+
- Fixing text scanner now treats files containing NUL bytes as binary data instead of misidentifying them as text
|
|
14
|
+
- Fixing mz5 HDF5 scanner typo in chromatogram dataset name
|
|
15
|
+
|
|
16
|
+
Version 2.1.1
|
|
17
|
+
-------------
|
|
18
|
+
|
|
19
|
+
- Fixing #141 deep scan no longer overrides valid binary format matches (e.g. JPEG) for files without extensions (thanks to marph91)
|
|
20
|
+
|
|
4
21
|
Version 2.1.0
|
|
5
22
|
-------------
|
|
6
23
|
|
|
@@ -32,10 +32,13 @@ if os.getenv("PUREMAGIC_DEEPSCAN") != "0":
|
|
|
32
32
|
mpeg_audio_scanner,
|
|
33
33
|
hdf5_scanner,
|
|
34
34
|
cfbf_scanner,
|
|
35
|
+
ogg_scanner,
|
|
36
|
+
asf_scanner,
|
|
37
|
+
ebml_scanner,
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
__author__ = "Chris Griffith"
|
|
38
|
-
__version__ = "2.
|
|
41
|
+
__version__ = "2.2.0"
|
|
39
42
|
__all__ = [
|
|
40
43
|
"magic_file",
|
|
41
44
|
"magic_string",
|
|
@@ -206,19 +209,20 @@ def identify_all(header: bytes, footer: bytes, ext=None) -> list[PureMagicWithCo
|
|
|
206
209
|
return determine_confidence(matches, ext)
|
|
207
210
|
|
|
208
211
|
|
|
209
|
-
def perform_magic(header: bytes, footer: bytes, mime: bool, ext=None, filename=None) -> str:
|
|
212
|
+
def perform_magic(header: bytes | None, footer: bytes | None, mime: bool | None, ext=None, filename=None) -> str:
|
|
210
213
|
"""Discover what type of file it is based on the incoming string"""
|
|
211
214
|
if not header:
|
|
212
215
|
raise PureValueError("Input was empty")
|
|
216
|
+
if not footer:
|
|
217
|
+
footer = b""
|
|
213
218
|
infos = identify_all(header, footer, ext)
|
|
214
219
|
if filename and os.path.isfile(filename) and os.getenv("PUREMAGIC_DEEPSCAN") != "0":
|
|
215
|
-
results = run_deep_scan(infos, filename, header, footer, raise_on_none=
|
|
216
|
-
if results:
|
|
217
|
-
if results[0].extension == "":
|
|
218
|
-
raise PureError("Could not identify file")
|
|
220
|
+
results = run_deep_scan(infos, filename, header, footer, raise_on_none=not infos)
|
|
221
|
+
if results and results[0].extension != "":
|
|
219
222
|
if mime:
|
|
220
223
|
return results[0].mime_type
|
|
221
224
|
return results[0].extension or ""
|
|
225
|
+
# Deep scan returned empty extension or no results — fall through to original matches
|
|
222
226
|
if not infos:
|
|
223
227
|
raise PureError("Could not identify file")
|
|
224
228
|
info = infos[0]
|
|
@@ -467,6 +471,14 @@ def single_deep_scan(
|
|
|
467
471
|
return result
|
|
468
472
|
case cfbf_scanner.match_bytes | cfbf_scanner.match_bytes_short:
|
|
469
473
|
return cfbf_scanner.main(filename, head, foot)
|
|
474
|
+
case ogg_scanner.match_bytes:
|
|
475
|
+
result = ogg_scanner.main(filename, head, foot)
|
|
476
|
+
if result and result.confidence > confidence:
|
|
477
|
+
return result
|
|
478
|
+
case asf_scanner.match_bytes:
|
|
479
|
+
return asf_scanner.main(filename, head, foot)
|
|
480
|
+
case ebml_scanner.match_bytes:
|
|
481
|
+
return ebml_scanner.main(filename, head, foot)
|
|
470
482
|
|
|
471
483
|
if eml_result := text_scanner.eml_check(head):
|
|
472
484
|
return eml_result
|
|
@@ -556,13 +568,18 @@ def run_deep_scan(
|
|
|
556
568
|
|
|
557
569
|
# No specific scanner matched — try the catch-all text scanner
|
|
558
570
|
# Only override when existing matches are very low confidence (e.g. 2-byte BOM signatures)
|
|
559
|
-
|
|
571
|
+
# Only let the catch-all text scanner override when existing matches are
|
|
572
|
+
# generic text types (e.g. BOM-only signatures). If the magic database
|
|
573
|
+
# already identified a specific non-text file type, trust it over a generic text guess.
|
|
574
|
+
best_mime = matches[0].mime_type or ""
|
|
575
|
+
is_generic = best_mime.startswith("text/") or best_mime == "application/octet-stream" or not best_mime
|
|
576
|
+
if matches[0].confidence < 0.5 and is_generic:
|
|
560
577
|
try:
|
|
561
578
|
result = catch_all_deep_scan(filename, head, foot)
|
|
562
579
|
except Exception:
|
|
563
580
|
pass
|
|
564
581
|
else:
|
|
565
|
-
if result and result.confidence > matches[0].confidence:
|
|
582
|
+
if result and result.extension and result.confidence > matches[0].confidence:
|
|
566
583
|
return [
|
|
567
584
|
PureMagicWithConfidence(
|
|
568
585
|
confidence=result.confidence,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
from puremagic.scanners.helpers import Match
|
|
5
|
+
|
|
6
|
+
# ASF Header Object GUID
|
|
7
|
+
match_bytes = b"\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c"
|
|
8
|
+
|
|
9
|
+
_STREAM_PROPS_GUID = b"\x91\x07\xdc\xb7\xb7\xa9\xcf\x11\x8e\xe6\x00\xc0\x0c\x20\x53\x65"
|
|
10
|
+
_AUDIO_MEDIA_GUID = b"\x40\x9e\x69\xf8\x4d\x5b\xcf\x11\xa8\xfd\x00\x80\x5f\x5c\x44\x2b"
|
|
11
|
+
_VIDEO_MEDIA_GUID = b"\xc0\xef\x19\xbc\x4d\x5b\xcf\x11\xa8\xfd\x00\x80\x5f\x5c\x44\x2b"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(file_path: os.PathLike, head: bytes, foot: bytes) -> Match | None:
|
|
15
|
+
if not head or len(head) < 30:
|
|
16
|
+
return None
|
|
17
|
+
if head[:16] != match_bytes:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
header_size = struct.unpack_from("<Q", head, 16)[0]
|
|
21
|
+
obj_count = struct.unpack_from("<I", head, 24)[0]
|
|
22
|
+
|
|
23
|
+
# Read the full ASF header if our head buffer is too small
|
|
24
|
+
if header_size > len(head):
|
|
25
|
+
try:
|
|
26
|
+
with open(file_path, "rb") as f:
|
|
27
|
+
data = f.read(min(int(header_size), 65536))
|
|
28
|
+
except (OSError, ValueError):
|
|
29
|
+
return None
|
|
30
|
+
else:
|
|
31
|
+
data = head
|
|
32
|
+
|
|
33
|
+
has_audio = False
|
|
34
|
+
has_video = False
|
|
35
|
+
offset = 30 # Past header GUID(16) + size(8) + count(4) + reserved(2)
|
|
36
|
+
|
|
37
|
+
for _ in range(min(obj_count, 50)):
|
|
38
|
+
if offset + 24 > len(data):
|
|
39
|
+
break
|
|
40
|
+
obj_guid = data[offset : offset + 16]
|
|
41
|
+
obj_size = struct.unpack_from("<Q", data, offset + 16)[0]
|
|
42
|
+
if obj_size < 24:
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if obj_guid == _STREAM_PROPS_GUID and offset + 40 <= len(data):
|
|
46
|
+
stream_type = data[offset + 24 : offset + 40]
|
|
47
|
+
if stream_type == _VIDEO_MEDIA_GUID:
|
|
48
|
+
has_video = True
|
|
49
|
+
elif stream_type == _AUDIO_MEDIA_GUID:
|
|
50
|
+
has_audio = True
|
|
51
|
+
|
|
52
|
+
offset += int(obj_size)
|
|
53
|
+
|
|
54
|
+
if has_video:
|
|
55
|
+
return Match(".wmv", "Windows Media Video", "video/x-ms-wmv")
|
|
56
|
+
if has_audio:
|
|
57
|
+
return Match(".wma", "Windows Media Audio", "audio/x-ms-wma")
|
|
58
|
+
|
|
59
|
+
return Match(".asf", "Advanced Systems Format", "video/x-ms-asf")
|
|
@@ -24,6 +24,32 @@ _PREFIX_MATCHES = [
|
|
|
24
24
|
("__substg1.0_", ".msg", "Outlook Message", "application/vnd.ms-outlook"),
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
+
# Multi-stream detection: all listed streams must be present.
|
|
28
|
+
# Each entry: (required_streams, extension, name, mime_type)
|
|
29
|
+
_MULTI_STREAM_MATCHES = [
|
|
30
|
+
(("_StringPool", "_StringData"), ".msi", "Windows Installer Package", "application/x-msi"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Root directory entry CLSIDs that identify specific formats.
|
|
34
|
+
# CLSIDs are stored in mixed-endian format in CFBF files.
|
|
35
|
+
# Each entry: (clsid_bytes, extension, name, mime_type)
|
|
36
|
+
_CLSID_MATCHES = [
|
|
37
|
+
# Microsoft Project 98/2000/2002/2003: {74b78f3a-c8c8-11d1-be11-00c04fb6faf1}
|
|
38
|
+
(
|
|
39
|
+
b"\x3a\x8f\xb7\x74\xc8\xc8\xd1\x11\xbe\x11\x00\xc0\x4f\xb6\xfa\xf1",
|
|
40
|
+
".mpp",
|
|
41
|
+
"Microsoft Project",
|
|
42
|
+
"application/vnd.ms-project",
|
|
43
|
+
),
|
|
44
|
+
# Microsoft Project 4.x: {72fd3320-9a05-11cf-85a4-00a0c904de5f}
|
|
45
|
+
(
|
|
46
|
+
b"\x20\x33\xfd\x72\x05\x9a\xcf\x11\x85\xa4\x00\xa0\xc9\x04\xde\x5f",
|
|
47
|
+
".mpp",
|
|
48
|
+
"Microsoft Project",
|
|
49
|
+
"application/vnd.ms-project",
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
27
53
|
|
|
28
54
|
def _extract_stream_names(dir_data: bytes) -> set[str]:
|
|
29
55
|
"""Parse CFBF directory entries and return the set of stream/storage names."""
|
|
@@ -45,8 +71,19 @@ def _extract_stream_names(dir_data: bytes) -> set[str]:
|
|
|
45
71
|
return names
|
|
46
72
|
|
|
47
73
|
|
|
48
|
-
def
|
|
49
|
-
"""
|
|
74
|
+
def _extract_root_clsid(dir_data: bytes) -> bytes | None:
|
|
75
|
+
"""Extract the CLSID from the root directory entry (obj_type 5)."""
|
|
76
|
+
for i in range(0, len(dir_data), 128):
|
|
77
|
+
entry = dir_data[i : i + 128]
|
|
78
|
+
if len(entry) < 96:
|
|
79
|
+
break
|
|
80
|
+
if entry[66] == 5: # Root storage
|
|
81
|
+
return entry[80:96]
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _identify_format(stream_names: set[str], dir_data: bytes) -> Match | None:
|
|
86
|
+
"""Match stream names and CLSIDs against known CFBF format signatures."""
|
|
50
87
|
# Check prefix matches first (e.g. __substg1.0_ for MSG)
|
|
51
88
|
for name in stream_names:
|
|
52
89
|
for prefix, ext, fmt_name, mime in _PREFIX_MATCHES:
|
|
@@ -58,6 +95,18 @@ def _identify_format(stream_names: set[str]) -> Match | None:
|
|
|
58
95
|
if stream_name in stream_names:
|
|
59
96
|
return Match(ext, fmt_name, mime)
|
|
60
97
|
|
|
98
|
+
# Check multi-stream matches (all required streams must be present)
|
|
99
|
+
for required_streams, ext, fmt_name, mime in _MULTI_STREAM_MATCHES:
|
|
100
|
+
if all(s in stream_names for s in required_streams):
|
|
101
|
+
return Match(ext, fmt_name, mime)
|
|
102
|
+
|
|
103
|
+
# Check root CLSID
|
|
104
|
+
root_clsid = _extract_root_clsid(dir_data)
|
|
105
|
+
if root_clsid:
|
|
106
|
+
for clsid, ext, fmt_name, mime in _CLSID_MATCHES:
|
|
107
|
+
if root_clsid == clsid:
|
|
108
|
+
return Match(ext, fmt_name, mime)
|
|
109
|
+
|
|
61
110
|
return None
|
|
62
111
|
|
|
63
112
|
|
|
@@ -94,4 +143,4 @@ def main(file_path: os.PathLike, head: bytes, foot: bytes) -> Match | None:
|
|
|
94
143
|
return None
|
|
95
144
|
|
|
96
145
|
stream_names = _extract_stream_names(dir_data)
|
|
97
|
-
return _identify_format(stream_names)
|
|
146
|
+
return _identify_format(stream_names, dir_data)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from puremagic.scanners.helpers import Match
|
|
4
|
+
|
|
5
|
+
match_bytes = b"\x1a\x45\xdf\xa3"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(file_path: os.PathLike, head: bytes, foot: bytes) -> Match | None:
|
|
9
|
+
if not head or len(head) < 8:
|
|
10
|
+
return None
|
|
11
|
+
if head[:4] != match_bytes:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
# Search for DocType string in the EBML header (first 64 bytes)
|
|
15
|
+
search_area = head[:64]
|
|
16
|
+
if b"webm" in search_area:
|
|
17
|
+
return Match(".webm", "WebM Video", "video/webm")
|
|
18
|
+
if b"matroska" in search_area:
|
|
19
|
+
return Match(".mkv", "Matroska Video", "video/x-matroska")
|
|
20
|
+
|
|
21
|
+
return None
|
|
@@ -32,7 +32,7 @@ _SUBTYPES = [
|
|
|
32
32
|
"application/x-biom2",
|
|
33
33
|
),
|
|
34
34
|
# mz5 - mass spectrometry
|
|
35
|
-
([], [b"/SpectrumMetaData", b"/
|
|
35
|
+
([], [b"/SpectrumMetaData", b"/ChromatogramList"], 1, ".mz5", "mz5 mass spectrometry data", "application/x-mz5"),
|
|
36
36
|
# h5mlm - ML model
|
|
37
37
|
([], [b"model_type", b"h5mlm"], 1, ".h5mlm", "HDF5 ML model", "application/x-h5mlm"),
|
|
38
38
|
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from puremagic.scanners.helpers import Match
|
|
4
|
+
|
|
5
|
+
match_bytes = b"OggS"
|
|
6
|
+
|
|
7
|
+
# Ogg codec identification signatures found at the start of the first page payload.
|
|
8
|
+
# Each entry: (codec_id_bytes, extension, name, mime_type)
|
|
9
|
+
_OGG_CODEC_MAP = [
|
|
10
|
+
(b"\x01vorbis", ".ogg", "Ogg Vorbis Audio", "audio/ogg"),
|
|
11
|
+
(b"OpusHead", ".opus", "Ogg Opus Audio", "audio/ogg"),
|
|
12
|
+
(b"\x80theora", ".ogv", "Ogg Theora Video", "video/ogg"),
|
|
13
|
+
(b"\x7fFLAC", ".oga", "Ogg FLAC Audio", "audio/ogg"),
|
|
14
|
+
(b"Speex ", ".spx", "Ogg Speex Audio", "audio/ogg"),
|
|
15
|
+
(b"fishead\x00", ".ogv", "Ogg Annodex", "video/ogg"),
|
|
16
|
+
(b"\x01video", ".ogm", "OGM Video", "video/x-ogm+ogg"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(file_path: os.PathLike, head: bytes, foot: bytes) -> Match | None:
|
|
21
|
+
if not head or len(head) < 28:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
# Verify OggS capture pattern, version 0, and beginning-of-stream flag
|
|
25
|
+
if head[:4] != match_bytes or head[4] != 0 or not (head[5] & 0x02):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
seg_count = head[26]
|
|
29
|
+
payload_start = 27 + seg_count
|
|
30
|
+
|
|
31
|
+
if payload_start >= len(head):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
payload = head[payload_start:]
|
|
35
|
+
for codec_id, ext, name, mime in _OGG_CODEC_MAP:
|
|
36
|
+
if payload.startswith(codec_id):
|
|
37
|
+
return Match(ext, name, mime, confidence=0.9)
|
|
38
|
+
|
|
39
|
+
return None
|
|
@@ -200,6 +200,10 @@ def main(file_path: os.PathLike | str, _, __) -> Match | None:
|
|
|
200
200
|
if len(head) < 8:
|
|
201
201
|
return Match("", "very short file", "application/octet-stream", confidence=0.5)
|
|
202
202
|
|
|
203
|
+
# NUL bytes indicate binary data, but skip this check for UTF-16 (which has NUL bytes naturally)
|
|
204
|
+
if b"\x00" in head and head[:2] not in (b"\xff\xfe", b"\xfe\xff"):
|
|
205
|
+
return Match("", "data", "application/octet-stream", confidence=0.5)
|
|
206
|
+
|
|
203
207
|
try:
|
|
204
208
|
text, encoding = decode_any(head)
|
|
205
209
|
except TypeError:
|
|
@@ -8,6 +8,120 @@ match_bytes = b"PK\x03\x04"
|
|
|
8
8
|
office_macro_enable_match = b"macroEnabled"
|
|
9
9
|
|
|
10
10
|
application_re = re.compile(b"<Application>(.*)</Application>")
|
|
11
|
+
_content_type_re = re.compile(rb'ContentType="([^"]*main[^"]*)"')
|
|
12
|
+
|
|
13
|
+
# Maps OOXML main content type fragments (from [Content_Types].xml) to
|
|
14
|
+
# (extension, name, mime_type). Based on ECMA-376 Part 2.
|
|
15
|
+
_OOXML_CONTENT_TYPE_MAP: dict[str, tuple[str, str, str]] = {
|
|
16
|
+
# Word
|
|
17
|
+
"wordprocessingml.document.main+xml": (
|
|
18
|
+
".docx",
|
|
19
|
+
"Word Document",
|
|
20
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
21
|
+
),
|
|
22
|
+
"wordprocessingml.template.main+xml": (
|
|
23
|
+
".dotx",
|
|
24
|
+
"Word Template",
|
|
25
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
26
|
+
),
|
|
27
|
+
"ms-word.document.macroEnabled.main+xml": (
|
|
28
|
+
".docm",
|
|
29
|
+
"Word Document (Macro-Enabled)",
|
|
30
|
+
"application/vnd.ms-word.document.macroEnabled.12",
|
|
31
|
+
),
|
|
32
|
+
"ms-word.template.macroEnabledTemplate.main+xml": (
|
|
33
|
+
".dotm",
|
|
34
|
+
"Word Template (Macro-Enabled)",
|
|
35
|
+
"application/vnd.ms-word.template.macroEnabled.12",
|
|
36
|
+
),
|
|
37
|
+
# Excel
|
|
38
|
+
"spreadsheetml.sheet.main+xml": (
|
|
39
|
+
".xlsx",
|
|
40
|
+
"Excel Spreadsheet",
|
|
41
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
42
|
+
),
|
|
43
|
+
"spreadsheetml.template.main+xml": (
|
|
44
|
+
".xltx",
|
|
45
|
+
"Excel Template",
|
|
46
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
47
|
+
),
|
|
48
|
+
"ms-excel.sheet.macroEnabled.main+xml": (
|
|
49
|
+
".xlsm",
|
|
50
|
+
"Excel Spreadsheet (Macro-Enabled)",
|
|
51
|
+
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
52
|
+
),
|
|
53
|
+
"ms-excel.template.macroEnabled.main+xml": (
|
|
54
|
+
".xltm",
|
|
55
|
+
"Excel Template (Macro-Enabled)",
|
|
56
|
+
"application/vnd.ms-excel.template.macroEnabled.12",
|
|
57
|
+
),
|
|
58
|
+
"ms-excel.sheet.binary.macroEnabled.main": (
|
|
59
|
+
".xlsb",
|
|
60
|
+
"Excel Binary Workbook",
|
|
61
|
+
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
62
|
+
),
|
|
63
|
+
"ms-excel.addin.macroEnabled.main+xml": (
|
|
64
|
+
".xlam",
|
|
65
|
+
"Excel Add-In (Macro-Enabled)",
|
|
66
|
+
"application/vnd.ms-excel.addin.macroEnabled.12",
|
|
67
|
+
),
|
|
68
|
+
# PowerPoint
|
|
69
|
+
"presentationml.presentation.main+xml": (
|
|
70
|
+
".pptx",
|
|
71
|
+
"PowerPoint Presentation",
|
|
72
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
73
|
+
),
|
|
74
|
+
"presentationml.template.main+xml": (
|
|
75
|
+
".potx",
|
|
76
|
+
"PowerPoint Template",
|
|
77
|
+
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
78
|
+
),
|
|
79
|
+
"ms-powerpoint.presentation.macroEnabled.main+xml": (
|
|
80
|
+
".pptm",
|
|
81
|
+
"PowerPoint Presentation (Macro-Enabled)",
|
|
82
|
+
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
83
|
+
),
|
|
84
|
+
"ms-powerpoint.template.macroEnabled.main+xml": (
|
|
85
|
+
".potm",
|
|
86
|
+
"PowerPoint Template (Macro-Enabled)",
|
|
87
|
+
"application/vnd.ms-powerpoint.template.macroEnabled.12",
|
|
88
|
+
),
|
|
89
|
+
"presentationml.slideshow.main+xml": (
|
|
90
|
+
".ppsx",
|
|
91
|
+
"PowerPoint Slideshow",
|
|
92
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
93
|
+
),
|
|
94
|
+
"ms-powerpoint.slideshow.macroEnabled.main+xml": (
|
|
95
|
+
".ppsm",
|
|
96
|
+
"PowerPoint Slideshow (Macro-Enabled)",
|
|
97
|
+
"application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
98
|
+
),
|
|
99
|
+
"ms-powerpoint.addin.macroEnabled.main+xml": (
|
|
100
|
+
".ppam",
|
|
101
|
+
"PowerPoint Add-In (Macro-Enabled)",
|
|
102
|
+
"application/vnd.ms-powerpoint.addin.macroEnabled",
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _detect_from_content_types(zip_file: ZipFile) -> Match | None:
|
|
108
|
+
"""Detect OOXML format from [Content_Types].xml main content types.
|
|
109
|
+
|
|
110
|
+
This is the ECMA-376 standard approach, working with all compliant
|
|
111
|
+
implementations (Microsoft Office, LibreOffice, Google Docs, etc.).
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
ct_data = zip_file.read("[Content_Types].xml")
|
|
115
|
+
except KeyError:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
for match in _content_type_re.finditer(ct_data):
|
|
119
|
+
content_type = match.group(1).decode("utf-8")
|
|
120
|
+
for key, (ext, name, mime) in _OOXML_CONTENT_TYPE_MAP.items():
|
|
121
|
+
if key in content_type:
|
|
122
|
+
return Match(ext, name, mime)
|
|
123
|
+
|
|
124
|
+
return None
|
|
11
125
|
|
|
12
126
|
|
|
13
127
|
def open_office_check(internal_files: list[str], zip_file: ZipFile, extension: str | None = None) -> Match | None:
|
|
@@ -32,9 +146,10 @@ def open_office_check(internal_files: list[str], zip_file: ZipFile, extension: s
|
|
|
32
146
|
return None
|
|
33
147
|
|
|
34
148
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
149
|
+
def _detect_from_application(
|
|
150
|
+
internal_files: list[str], zip_file: ZipFile, extension: str | None = None
|
|
151
|
+
) -> Match | None:
|
|
152
|
+
"""Fallback detection using docProps/app.xml Application tag."""
|
|
38
153
|
if "docProps/app.xml" not in internal_files:
|
|
39
154
|
return None
|
|
40
155
|
app_type_matches = application_re.search(zip_file.read("docProps/app.xml"))
|
|
@@ -44,22 +159,30 @@ def office_check(internal_files: list[str], zip_file: ZipFile, extension: str |
|
|
|
44
159
|
|
|
45
160
|
if "PowerPoint" in application_type:
|
|
46
161
|
if extension:
|
|
162
|
+
if extension == "pptm":
|
|
163
|
+
return Match(".pptm", application_type, "application/vnd.ms-powerpoint.presentation.macroEnabled.12")
|
|
47
164
|
if extension == "ppsm":
|
|
48
165
|
return Match(".ppsm", application_type, "application/vnd.ms-powerpoint.slideshow.macroEnabled.12")
|
|
166
|
+
if extension == "ppsx":
|
|
167
|
+
return Match(
|
|
168
|
+
".ppsx",
|
|
169
|
+
application_type,
|
|
170
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
171
|
+
)
|
|
49
172
|
if extension == "potm":
|
|
50
173
|
return Match(".potm", application_type, "application/vnd.ms-powerpoint.template.macroEnabled.12")
|
|
51
174
|
if extension == "potx":
|
|
52
175
|
return Match(
|
|
53
|
-
"potx",
|
|
176
|
+
".potx",
|
|
54
177
|
application_type,
|
|
55
178
|
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
56
179
|
)
|
|
57
180
|
if extension == "ppam":
|
|
58
181
|
return Match(".ppam", application_type, "application/vnd.ms-powerpoint.addin.macroEnabled")
|
|
59
182
|
if office_macro_enable_match in zip_file.read("[Content_Types].xml"):
|
|
60
|
-
return Match(".
|
|
183
|
+
return Match(".pptm", application_type, "application/vnd.ms-powerpoint.presentation.macroEnabled.12")
|
|
61
184
|
return Match(
|
|
62
|
-
"pptx",
|
|
185
|
+
".pptx",
|
|
63
186
|
application_type,
|
|
64
187
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
65
188
|
)
|
|
@@ -75,7 +198,7 @@ def office_check(internal_files: list[str], zip_file: ZipFile, extension: str |
|
|
|
75
198
|
return Match(".xltm", application_type, "application/vnd.ms-excel.template.macroEnabled.12")
|
|
76
199
|
if extension == "xltx":
|
|
77
200
|
return Match(
|
|
78
|
-
"xltx",
|
|
201
|
+
".xltx",
|
|
79
202
|
application_type,
|
|
80
203
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
81
204
|
)
|
|
@@ -91,19 +214,32 @@ def office_check(internal_files: list[str], zip_file: ZipFile, extension: str |
|
|
|
91
214
|
return Match(".dotm", application_type, "application/vnd.ms-word.template.macroEnabled.12")
|
|
92
215
|
if extension == "dotx":
|
|
93
216
|
return Match(
|
|
94
|
-
"dotx",
|
|
217
|
+
".dotx",
|
|
95
218
|
application_type,
|
|
96
219
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
97
220
|
)
|
|
98
221
|
if office_macro_enable_match in zip_file.read("[Content_Types].xml"):
|
|
99
222
|
return Match(".docm", application_type, "application/vnd.ms-word.document.macroEnabled.12")
|
|
100
223
|
return Match(
|
|
101
|
-
"docx", application_type, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
224
|
+
".docx", application_type, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
102
225
|
)
|
|
103
226
|
|
|
104
227
|
return None
|
|
105
228
|
|
|
106
229
|
|
|
230
|
+
def office_check(internal_files: list[str], zip_file: ZipFile, extension: str | None = None) -> Match | None:
|
|
231
|
+
if "[Content_Types].xml" not in internal_files:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# Primary: content-type-based detection (works with all OOXML creators)
|
|
235
|
+
result = _detect_from_content_types(zip_file)
|
|
236
|
+
if result:
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
# Fallback: application-based detection for non-standard OOXML packages
|
|
240
|
+
return _detect_from_application(internal_files, zip_file, extension)
|
|
241
|
+
|
|
242
|
+
|
|
107
243
|
def jar_check(internal_files: list[str], zip_file: ZipFile) -> Match | None:
|
|
108
244
|
if "META-INF/MANIFEST.MF" not in internal_files:
|
|
109
245
|
return None
|
|
@@ -156,8 +292,6 @@ def cbz_check(internal_files: list[str], extension: str) -> Match | None:
|
|
|
156
292
|
|
|
157
293
|
def main(file_path: os.PathLike, _, __) -> Match | None:
|
|
158
294
|
extension = str(file_path).split(".")[-1].lower()
|
|
159
|
-
if extension == "zip" and not str(file_path).endswith(".fb2.zip"):
|
|
160
|
-
return Match(".zip", "ZIP archive", "application/zip")
|
|
161
295
|
|
|
162
296
|
with ZipFile(file_path) as myzip:
|
|
163
297
|
internal_files = myzip.namelist()
|
|
@@ -189,4 +323,5 @@ def main(file_path: os.PathLike, _, __) -> Match | None:
|
|
|
189
323
|
if cbz_result:
|
|
190
324
|
return cbz_result
|
|
191
325
|
|
|
192
|
-
return
|
|
326
|
+
# No specific format detected — return generic ZIP (same confidence as other matches)
|
|
327
|
+
return Match(".zip", "ZIP archive", "application/zip")
|
|
@@ -24,11 +24,14 @@ puremagic.egg-info/dependency_links.txt
|
|
|
24
24
|
puremagic.egg-info/entry_points.txt
|
|
25
25
|
puremagic.egg-info/top_level.txt
|
|
26
26
|
puremagic/scanners/__init__.py
|
|
27
|
+
puremagic/scanners/asf_scanner.py
|
|
27
28
|
puremagic/scanners/cfbf_scanner.py
|
|
29
|
+
puremagic/scanners/ebml_scanner.py
|
|
28
30
|
puremagic/scanners/hdf5_scanner.py
|
|
29
31
|
puremagic/scanners/helpers.py
|
|
30
32
|
puremagic/scanners/json_scanner.py
|
|
31
33
|
puremagic/scanners/mpeg_audio_scanner.py
|
|
34
|
+
puremagic/scanners/ogg_scanner.py
|
|
32
35
|
puremagic/scanners/pdf_scanner.py
|
|
33
36
|
puremagic/scanners/python_scanner.py
|
|
34
37
|
puremagic/scanners/sndhdr_scanner.py
|
|
@@ -61,9 +64,12 @@ test/resources/audio/test.aac
|
|
|
61
64
|
test/resources/audio/test.aif
|
|
62
65
|
test/resources/audio/test.flac
|
|
63
66
|
test/resources/audio/test.mp3
|
|
67
|
+
test/resources/audio/test.oga
|
|
68
|
+
test/resources/audio/test.ogg
|
|
64
69
|
test/resources/audio/test.opus
|
|
65
70
|
test/resources/audio/test.sndr
|
|
66
71
|
test/resources/audio/test.wav
|
|
72
|
+
test/resources/audio/test.wma
|
|
67
73
|
test/resources/audio/test_mp3_vbr_info_128k_notags.mp3
|
|
68
74
|
test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3
|
|
69
75
|
test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3
|
|
@@ -134,4 +140,6 @@ test/resources/video/test.jxsv
|
|
|
134
140
|
test/resources/video/test.mkv
|
|
135
141
|
test/resources/video/test.mp4
|
|
136
142
|
test/resources/video/test.mpg
|
|
143
|
+
test/resources/video/test.ogv
|
|
144
|
+
test/resources/video/test.webm
|
|
137
145
|
test/resources/video/test.wmv
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|