puremagic 2.0.2__tar.gz → 2.1.1__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.1/.claude/skills/changelog.md +53 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/.pre-commit-config.yaml +11 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/CHANGELOG.md +14 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/PKG-INFO +1 -1
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/magic_data.json +1 -1
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/main.py +95 -8
- puremagic-2.1.1/puremagic/scanners/cfbf_scanner.py +97 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/mpeg_audio_scanner.py +3 -3
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic.egg-info/PKG-INFO +1 -1
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic.egg-info/SOURCES.txt +3 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/pyproject.toml +2 -0
- puremagic-2.1.1/test/resources/office/test.msg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/test_common_extensions.py +68 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/test_scanners.py +20 -1
- {puremagic-2.0.2 → puremagic-2.1.1}/uv.lock +14 -3
- {puremagic-2.0.2 → puremagic-2.1.1}/.coveragerc +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/.github/workflows/pythonpublish.yml +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/.github/workflows/tests.yml +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/.gitignore +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/AUTHORS.rst +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/LICENSE +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/MANIFEST.in +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/README.rst +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/__init__.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/__main__.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/py.typed +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/__init__.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/hdf5_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/helpers.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/json_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/pdf_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/python_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/sndhdr_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/text_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic/scanners/zip_scanner.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic.egg-info/dependency_links.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic.egg-info/entry_points.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/puremagic.egg-info/top_level.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/requirements-test.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/scripts/parse_ftk_kessler_sigs.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/setup.cfg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/__init__.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/common.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.7z +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.big_endian.pcapng +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.bz2 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.cb7 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.cbt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.cbz +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.fb2.zip +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.fbz +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.gz +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.little_endian.pcapng +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.pcapng +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.rar +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.tar +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.txt.lz4 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.txt.zst +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.xz +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/archive/test.zip +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.aac +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.aif +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.flac +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.opus +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.sndr +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test.wav +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_info_128k_notags.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_apev2_tagplus_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_notags.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_apev2_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/fake_file +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.avif +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.bmp +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.gif +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.ico +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.jpg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.png +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.psd +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.svg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.tga +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.tif +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.webp +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test.xcf +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test_varriant.svg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/images/test_varriant_2.svg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/media/test (single).vmdk +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/media/test (split).vmdk +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/media/test.iso +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/media/test.mdf +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/media/test.swf +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.doc +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.docm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.docx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.dotm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.dotx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.eml +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.fb2 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.odp +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.ods +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.odt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.pdf +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.potm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.potx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.ppt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.pptm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.pptx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xls +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xlsb +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xlsm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xlsx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xltm +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test.xltx +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/test_utf16le.csv +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/text_cr.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/text_crlf.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/office/text_lf.txt +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/system/test.exe +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/system/test.json +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/system/test.puremagic_multi_footer +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/system/test.py +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/system/test_list.json +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.3g2 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.avi +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.flv +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.jxsv +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.mkv +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.mp4 +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.mpg +0 -0
- {puremagic-2.0.2 → puremagic-2.1.1}/test/resources/video/test.wmv +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: changelog
|
|
3
|
+
description: Update the CHANGELOG.md changelog file with new entries
|
|
4
|
+
user_invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Changelog Skill
|
|
8
|
+
|
|
9
|
+
When updating the `CHANGELOG.md` file, follow these rules:
|
|
10
|
+
|
|
11
|
+
## Entry Format
|
|
12
|
+
|
|
13
|
+
Each entry is a single bullet point starting with `- `:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
- {Verb} {description}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Verbs and Ordering
|
|
20
|
+
|
|
21
|
+
Entries MUST use one of these four starting verbs, and MUST appear in this order within each version section:
|
|
22
|
+
|
|
23
|
+
1. **Adding** — new features
|
|
24
|
+
2. **Changing** — modifications to existing behavior
|
|
25
|
+
3. **Fixing** — bug fixes
|
|
26
|
+
4. **Removing** — removed features or deprecated items
|
|
27
|
+
|
|
28
|
+
## GitHub Issue Entries
|
|
29
|
+
|
|
30
|
+
- Entries that reference a GitHub issue include the issue number after the verb: `* Fixing #725 description...`
|
|
31
|
+
- Within each verb group, entries WITH issue numbers come FIRST, sorted by issue number ascending (smallest to largest)
|
|
32
|
+
- Entries WITHOUT issue numbers follow after
|
|
33
|
+
|
|
34
|
+
## Thanks Attribution
|
|
35
|
+
|
|
36
|
+
- When an entry references a GitHub issue, thank the issue author by their **GitHub display name** (not username)
|
|
37
|
+
- Look up the display name via `gh api users/{username} --jq '.name // .login'`
|
|
38
|
+
- Format: `(thanks to {display name})`
|
|
39
|
+
- If multiple people contributed (e.g., reporter and commenter with the fix), thank all of them
|
|
40
|
+
- The thanks attribution goes at the end of the entry
|
|
41
|
+
|
|
42
|
+
## Example
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Version 1.27
|
|
46
|
+
------------
|
|
47
|
+
|
|
48
|
+
- Adding #92 include py.typed in sdist (thanks to Nicholas Bollweg - bollwyvl)
|
|
49
|
+
- Adding #93 Improve PDF file detection, fix json description (thanks to Péter - peterekepeter)
|
|
50
|
+
- Adding new verbose output to command line with `-v` or `--verbose`
|
|
51
|
+
- Fixing #96 #86 stream does not work properly on opened small files (thanks to Felipe Lema and Andy - NebularNerd)
|
|
52
|
+
- Removing expected invalid WinZip signature
|
|
53
|
+
```
|
|
@@ -52,6 +52,17 @@ repos:
|
|
|
52
52
|
types: [python]
|
|
53
53
|
pass_filenames: false
|
|
54
54
|
|
|
55
|
+
- repo: https://github.com/codespell-project/codespell
|
|
56
|
+
rev: v2.4.1
|
|
57
|
+
hooks:
|
|
58
|
+
- id: codespell
|
|
59
|
+
args: [--ignore-words-list, "addin,caf,tye,wors"]
|
|
60
|
+
exclude: |
|
|
61
|
+
(?x)^(
|
|
62
|
+
test/resources/|
|
|
63
|
+
.*\.json$
|
|
64
|
+
)
|
|
65
|
+
|
|
55
66
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
56
67
|
rev: v2.16.2
|
|
57
68
|
hooks:
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
Version 2.1.1
|
|
5
|
+
-------------
|
|
6
|
+
|
|
7
|
+
- Fixing #141 deep scan no longer overrides valid binary format matches (e.g. JPEG) for files without extensions (thanks to marph91)
|
|
8
|
+
|
|
9
|
+
Version 2.1.0
|
|
10
|
+
-------------
|
|
11
|
+
|
|
12
|
+
- Adding #6 `from_extension()` and `magic_extension()` to look up MIME types by file extension
|
|
13
|
+
- Adding #119 CFBF/OLE2 scanner to distinguish Word, Excel, PowerPoint, Outlook MSG, Visio, and Publisher files (thanks to Simeon Stoykov and Andy - NebularNerd)
|
|
14
|
+
- Adding #139 codespell check (thanks to Christian Clauss)
|
|
15
|
+
- Adding `-e`/`--extension` CLI flag for extension-based MIME type lookup
|
|
16
|
+
- Fixing #104 WAV MIME type changed from `audio/wave` to `audio/wav` (thanks to Simon Willison and Andy - NebularNerd)
|
|
17
|
+
|
|
4
18
|
Version 2.0.2
|
|
5
19
|
-------------
|
|
6
20
|
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
["53434448", 8, ".sc2", "", "SimCity 2000 Map File"]
|
|
101
101
|
],
|
|
102
102
|
"52494646": [
|
|
103
|
-
["57415645", 8, ".wav", "audio/
|
|
103
|
+
["57415645", 8, ".wav", "audio/wav", "Waveform Audio File Format"],
|
|
104
104
|
["41564920", 8, ".avi", "video/avi", "Audio Video Interleave"],
|
|
105
105
|
["57454250", 8, ".webp", "image/webp", "WebP graphics file format"],
|
|
106
106
|
["41434f4e", 8, ".ani", "", "Animated cursor"],
|
|
@@ -31,17 +31,20 @@ if os.getenv("PUREMAGIC_DEEPSCAN") != "0":
|
|
|
31
31
|
sndhdr_scanner,
|
|
32
32
|
mpeg_audio_scanner,
|
|
33
33
|
hdf5_scanner,
|
|
34
|
+
cfbf_scanner,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
__author__ = "Chris Griffith"
|
|
37
|
-
__version__ = "2.
|
|
38
|
+
__version__ = "2.1.1"
|
|
38
39
|
__all__ = [
|
|
39
40
|
"magic_file",
|
|
40
41
|
"magic_string",
|
|
41
42
|
"magic_stream",
|
|
43
|
+
"magic_extension",
|
|
42
44
|
"from_file",
|
|
43
45
|
"from_string",
|
|
44
46
|
"from_stream",
|
|
47
|
+
"from_extension",
|
|
45
48
|
"ext_from_filename",
|
|
46
49
|
"PureError",
|
|
47
50
|
"PureMagic",
|
|
@@ -209,13 +212,12 @@ def perform_magic(header: bytes, footer: bytes, mime: bool, ext=None, filename=N
|
|
|
209
212
|
raise PureValueError("Input was empty")
|
|
210
213
|
infos = identify_all(header, footer, ext)
|
|
211
214
|
if filename and os.path.isfile(filename) and os.getenv("PUREMAGIC_DEEPSCAN") != "0":
|
|
212
|
-
results = run_deep_scan(infos, filename, header, footer, raise_on_none=
|
|
213
|
-
if results:
|
|
214
|
-
if results[0].extension == "":
|
|
215
|
-
raise PureError("Could not identify file")
|
|
215
|
+
results = run_deep_scan(infos, filename, header, footer, raise_on_none=not infos)
|
|
216
|
+
if results and results[0].extension != "":
|
|
216
217
|
if mime:
|
|
217
218
|
return results[0].mime_type
|
|
218
219
|
return results[0].extension or ""
|
|
220
|
+
# Deep scan returned empty extension or no results — fall through to original matches
|
|
219
221
|
if not infos:
|
|
220
222
|
raise PureError("Could not identify file")
|
|
221
223
|
info = infos[0]
|
|
@@ -390,6 +392,51 @@ def magic_stream(
|
|
|
390
392
|
return info
|
|
391
393
|
|
|
392
394
|
|
|
395
|
+
def from_extension(extension: str, mime: bool = True) -> str:
|
|
396
|
+
"""Look up a file type by its extension and return the MIME type or name.
|
|
397
|
+
|
|
398
|
+
:param extension: file extension with or without leading dot (e.g. ".pdf" or "pdf")
|
|
399
|
+
:param mime: Return mime type (default True), or human-readable name if False
|
|
400
|
+
:return: MIME type string or name
|
|
401
|
+
:raises PureError: if no match is found
|
|
402
|
+
"""
|
|
403
|
+
ext = extension.strip().lower()
|
|
404
|
+
if not ext.startswith("."):
|
|
405
|
+
ext = f".{ext}"
|
|
406
|
+
|
|
407
|
+
matches = []
|
|
408
|
+
for entry in chain(magic_header_array, magic_footer_array, extension_only_array):
|
|
409
|
+
if entry.extension == ext:
|
|
410
|
+
matches.append(entry)
|
|
411
|
+
|
|
412
|
+
if not matches:
|
|
413
|
+
raise PureError(f"Could not find extension {ext!r} in magic database")
|
|
414
|
+
|
|
415
|
+
# Prefer entries with longer byte_match (more specific signatures)
|
|
416
|
+
matches.sort(key=lambda x: len(x.byte_match), reverse=True)
|
|
417
|
+
best = matches[0]
|
|
418
|
+
return best.mime_type if mime else best.name
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def magic_extension(extension: str) -> list[PureMagicWithConfidence]:
|
|
422
|
+
"""Return all matches for a given file extension.
|
|
423
|
+
|
|
424
|
+
:param extension: file extension with or without leading dot (e.g. ".pdf" or "pdf")
|
|
425
|
+
:return: list of PureMagicWithConfidence sorted by confidence descending
|
|
426
|
+
"""
|
|
427
|
+
ext = extension.strip().lower()
|
|
428
|
+
if not ext.startswith("."):
|
|
429
|
+
ext = f".{ext}"
|
|
430
|
+
|
|
431
|
+
matches = []
|
|
432
|
+
for entry in chain(magic_header_array, magic_footer_array, extension_only_array):
|
|
433
|
+
if entry.extension == ext:
|
|
434
|
+
con = 0.8 if len(entry.byte_match) >= 9 else float(f"0.{len(entry.byte_match)}")
|
|
435
|
+
matches.append(PureMagicWithConfidence(confidence=con, **entry._asdict()))
|
|
436
|
+
|
|
437
|
+
return sorted(matches, key=lambda x: (x.confidence, len(x.byte_match)), reverse=True)
|
|
438
|
+
|
|
439
|
+
|
|
393
440
|
def single_deep_scan(
|
|
394
441
|
bytes_match: bytes | bytearray | None,
|
|
395
442
|
filename: os.PathLike | str,
|
|
@@ -417,6 +464,8 @@ def single_deep_scan(
|
|
|
417
464
|
result = mpeg_audio_scanner.main(filename, head, foot)
|
|
418
465
|
if result and result.confidence > confidence:
|
|
419
466
|
return result
|
|
467
|
+
case cfbf_scanner.match_bytes | cfbf_scanner.match_bytes_short:
|
|
468
|
+
return cfbf_scanner.main(filename, head, foot)
|
|
420
469
|
|
|
421
470
|
if eml_result := text_scanner.eml_check(head):
|
|
422
471
|
return eml_result
|
|
@@ -506,13 +555,18 @@ def run_deep_scan(
|
|
|
506
555
|
|
|
507
556
|
# No specific scanner matched — try the catch-all text scanner
|
|
508
557
|
# Only override when existing matches are very low confidence (e.g. 2-byte BOM signatures)
|
|
509
|
-
|
|
558
|
+
# Only let the catch-all text scanner override when existing matches are
|
|
559
|
+
# generic text types (e.g. BOM-only signatures). If the magic database
|
|
560
|
+
# already identified a specific non-text file type, trust it over a generic text guess.
|
|
561
|
+
best_mime = matches[0].mime_type or ""
|
|
562
|
+
is_generic = best_mime.startswith("text/") or best_mime == "application/octet-stream" or not best_mime
|
|
563
|
+
if matches[0].confidence < 0.5 and is_generic:
|
|
510
564
|
try:
|
|
511
565
|
result = catch_all_deep_scan(filename, head, foot)
|
|
512
566
|
except Exception:
|
|
513
567
|
pass
|
|
514
568
|
else:
|
|
515
|
-
if result and result.confidence > matches[0].confidence:
|
|
569
|
+
if result and result.extension and result.confidence > matches[0].confidence:
|
|
516
570
|
return [
|
|
517
571
|
PureMagicWithConfidence(
|
|
518
572
|
confidence=result.confidence,
|
|
@@ -544,10 +598,43 @@ def command_line_entry(*args):
|
|
|
544
598
|
help="Return the mime type instead of file type",
|
|
545
599
|
)
|
|
546
600
|
parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="Print verbose output")
|
|
547
|
-
parser.add_argument(
|
|
601
|
+
parser.add_argument(
|
|
602
|
+
"-e",
|
|
603
|
+
"--extension",
|
|
604
|
+
dest="extension",
|
|
605
|
+
help="Look up MIME type for a file extension (e.g. pdf or .pdf)",
|
|
606
|
+
)
|
|
607
|
+
parser.add_argument("files", nargs="*", type=Path)
|
|
548
608
|
parser.add_argument("--version", action="version", version=puremagic.__version__)
|
|
549
609
|
args = parser.parse_args(args if args else sys.argv[1:])
|
|
550
610
|
|
|
611
|
+
if args.extension:
|
|
612
|
+
if args.verbose:
|
|
613
|
+
matches = magic_extension(args.extension)
|
|
614
|
+
if not matches:
|
|
615
|
+
print(f"No matches found for extension '{args.extension}'")
|
|
616
|
+
else:
|
|
617
|
+
print(f"Total Possible Matches: {len(matches)}")
|
|
618
|
+
for i, result in enumerate(matches):
|
|
619
|
+
if i == 0:
|
|
620
|
+
print("\n\tBest Match")
|
|
621
|
+
else:
|
|
622
|
+
print(f"\tAlternative Match #{i}")
|
|
623
|
+
print(f"\tName: {result.name}")
|
|
624
|
+
print(f"\tConfidence: {int(result.confidence * 100)}%")
|
|
625
|
+
print(f"\tExtension: {result.extension}")
|
|
626
|
+
print(f"\tMime Type: {result.mime_type}")
|
|
627
|
+
print(f"\tByte Match: {result.byte_match}\n")
|
|
628
|
+
else:
|
|
629
|
+
try:
|
|
630
|
+
print(from_extension(args.extension, mime=not args.mime))
|
|
631
|
+
except PureError as e:
|
|
632
|
+
print(str(e))
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
if not args.files:
|
|
636
|
+
parser.error("the following arguments are required: files (or use -e/--extension)")
|
|
637
|
+
|
|
551
638
|
for fn in args.files:
|
|
552
639
|
if not fn.exists():
|
|
553
640
|
print(f"File '{fn}' does not exist!")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
from puremagic.scanners.helpers import Match
|
|
5
|
+
|
|
6
|
+
match_bytes = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
|
|
7
|
+
match_bytes_short = b"\xd0\xcf\x11\xe0"
|
|
8
|
+
|
|
9
|
+
# Stream names that identify specific CFBF-based formats, checked in priority order.
|
|
10
|
+
# Each entry: (stream_name, extension, name, mime_type)
|
|
11
|
+
# Using startswith for prefix matching where noted.
|
|
12
|
+
_STREAM_MATCHES = [
|
|
13
|
+
("__nameid_version1.0", ".msg", "Outlook Message", "application/vnd.ms-outlook"),
|
|
14
|
+
("PowerPoint Document", ".ppt", "PowerPoint Presentation", "application/vnd.ms-powerpoint"),
|
|
15
|
+
("Current User", ".ppt", "PowerPoint Presentation", "application/vnd.ms-powerpoint"),
|
|
16
|
+
("Workbook", ".xls", "Excel Spreadsheet", "application/vnd.ms-excel"),
|
|
17
|
+
("Book", ".xls", "Excel Spreadsheet", "application/vnd.ms-excel"),
|
|
18
|
+
("WordDocument", ".doc", "Word Document", "application/msword"),
|
|
19
|
+
("VisioDocument", ".vsd", "Visio Drawing", "application/x-visio"),
|
|
20
|
+
("Quill", ".pub", "Publisher Document", "application/x-mspublisher"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_PREFIX_MATCHES = [
|
|
24
|
+
("__substg1.0_", ".msg", "Outlook Message", "application/vnd.ms-outlook"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _extract_stream_names(dir_data: bytes) -> set[str]:
|
|
29
|
+
"""Parse CFBF directory entries and return the set of stream/storage names."""
|
|
30
|
+
names: set[str] = set()
|
|
31
|
+
for i in range(0, len(dir_data), 128):
|
|
32
|
+
entry = dir_data[i : i + 128]
|
|
33
|
+
if len(entry) < 128:
|
|
34
|
+
break
|
|
35
|
+
name_size = struct.unpack_from("<H", entry, 64)[0]
|
|
36
|
+
if name_size < 2 or name_size > 64:
|
|
37
|
+
continue
|
|
38
|
+
obj_type = entry[66]
|
|
39
|
+
# obj_type: 0=unknown, 1=storage, 2=stream, 5=root
|
|
40
|
+
if obj_type not in (1, 2, 5):
|
|
41
|
+
continue
|
|
42
|
+
name = entry[: name_size - 2].decode("utf-16-le", errors="ignore")
|
|
43
|
+
if name:
|
|
44
|
+
names.add(name)
|
|
45
|
+
return names
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _identify_format(stream_names: set[str]) -> Match | None:
|
|
49
|
+
"""Match stream names against known CFBF format signatures."""
|
|
50
|
+
# Check prefix matches first (e.g. __substg1.0_ for MSG)
|
|
51
|
+
for name in stream_names:
|
|
52
|
+
for prefix, ext, fmt_name, mime in _PREFIX_MATCHES:
|
|
53
|
+
if name.startswith(prefix):
|
|
54
|
+
return Match(ext, fmt_name, mime)
|
|
55
|
+
|
|
56
|
+
# Check exact stream name matches in priority order
|
|
57
|
+
for stream_name, ext, fmt_name, mime in _STREAM_MATCHES:
|
|
58
|
+
if stream_name in stream_names:
|
|
59
|
+
return Match(ext, fmt_name, mime)
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(file_path: os.PathLike, head: bytes, foot: bytes) -> Match | None:
|
|
65
|
+
if len(head) < 76:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Verify magic bytes
|
|
69
|
+
if head[:8] != match_bytes:
|
|
70
|
+
if head[:4] != match_bytes_short:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Parse CFBF header
|
|
74
|
+
sector_shift = struct.unpack_from("<H", head, 30)[0]
|
|
75
|
+
if sector_shift not in (9, 12):
|
|
76
|
+
return None
|
|
77
|
+
sector_size = 1 << sector_shift
|
|
78
|
+
|
|
79
|
+
first_dir_secid = struct.unpack_from("<i", head, 48)[0]
|
|
80
|
+
if first_dir_secid < 0:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Directory sector offset: header occupies first sector_size bytes
|
|
84
|
+
dir_offset = (first_dir_secid + 1) * sector_size
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with open(file_path, "rb") as f:
|
|
88
|
+
f.seek(dir_offset)
|
|
89
|
+
dir_data = f.read(sector_size)
|
|
90
|
+
except (OSError, ValueError):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if not dir_data:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
stream_names = _extract_stream_names(dir_data)
|
|
97
|
+
return _identify_format(stream_names)
|
|
@@ -228,7 +228,7 @@ class EndOfFileTags:
|
|
|
228
228
|
assert self.foot_string is not None
|
|
229
229
|
tag_size = 128
|
|
230
230
|
tag_plus_size = 227
|
|
231
|
-
speed_loc = 184 # Speed byte
|
|
231
|
+
speed_loc = 184 # Speed byte position in tag
|
|
232
232
|
combined_size = tag_plus_size + tag_size
|
|
233
233
|
|
|
234
234
|
if self.foot_size < combined_size: # TAG+ + ID3v1
|
|
@@ -466,7 +466,7 @@ class EndOfFileTags:
|
|
|
466
466
|
b"MCN",
|
|
467
467
|
)
|
|
468
468
|
id3v1_size = 128
|
|
469
|
-
max_tag_size = 1048576 # This is a
|
|
469
|
+
max_tag_size = 1048576 # This is a practical scan range of 1MB, Ape v2 in theory can be 4GB
|
|
470
470
|
combined_size = (max_tag_size + id3v1_size) if id3v1 else max_tag_size
|
|
471
471
|
|
|
472
472
|
if self.foot_size < combined_size: # APE OR APE + ID3v1
|
|
@@ -493,7 +493,7 @@ class EndOfFileTags:
|
|
|
493
493
|
f_size = struct.unpack("<I", self.foot_string[end_tag_start + 12 : end_tag_start + 16])[0]
|
|
494
494
|
|
|
495
495
|
if f_version == 1000: # v1
|
|
496
|
-
# Reach first key in tag, in APE the tag key name is
|
|
496
|
+
# Reach first key in tag, in APE the tag key name is preceded by 8 bytes associated with it.
|
|
497
497
|
first_key = combined_size - ((end_size + f_size) - apextag_size) + 8
|
|
498
498
|
# APE does not care about case for tag keys, but Title and UPPER are commonly accepted as standard
|
|
499
499
|
if not self.foot_string[first_key:].title().startswith(common_ape_keys):
|
|
@@ -10,6 +10,7 @@ pyproject.toml
|
|
|
10
10
|
requirements-test.txt
|
|
11
11
|
setup.cfg
|
|
12
12
|
uv.lock
|
|
13
|
+
.claude/skills/changelog.md
|
|
13
14
|
.github/workflows/pythonpublish.yml
|
|
14
15
|
.github/workflows/tests.yml
|
|
15
16
|
puremagic/__init__.py
|
|
@@ -23,6 +24,7 @@ puremagic.egg-info/dependency_links.txt
|
|
|
23
24
|
puremagic.egg-info/entry_points.txt
|
|
24
25
|
puremagic.egg-info/top_level.txt
|
|
25
26
|
puremagic/scanners/__init__.py
|
|
27
|
+
puremagic/scanners/cfbf_scanner.py
|
|
26
28
|
puremagic/scanners/hdf5_scanner.py
|
|
27
29
|
puremagic/scanners/helpers.py
|
|
28
30
|
puremagic/scanners/json_scanner.py
|
|
@@ -99,6 +101,7 @@ test/resources/office/test.dotm
|
|
|
99
101
|
test/resources/office/test.dotx
|
|
100
102
|
test/resources/office/test.eml
|
|
101
103
|
test/resources/office/test.fb2
|
|
104
|
+
test/resources/office/test.msg
|
|
102
105
|
test/resources/office/test.odp
|
|
103
106
|
test/resources/office/test.ods
|
|
104
107
|
test/resources/office/test.odt
|
|
@@ -30,6 +30,7 @@ scripts.puremagic = "puremagic.main:command_line_entry"
|
|
|
30
30
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
|
+
"codespell>=2.4.2",
|
|
33
34
|
"coverage>=7.13.4",
|
|
34
35
|
"poethepoet>=0.42.1",
|
|
35
36
|
"pre-commit>=4.5.1",
|
|
@@ -108,3 +109,4 @@ tasks.test = "pytest --cov=puremagic test/"
|
|
|
108
109
|
tasks.lint = "ruff check --fix"
|
|
109
110
|
tasks.format = "ruff format"
|
|
110
111
|
tasks.typecheck = "ty check"
|
|
112
|
+
tasks.spellcheck = "codespell --ignore-words-list=addin,caf,tye,wors --skip='*.json' puremagic/"
|
|
Binary file
|
|
@@ -220,6 +220,74 @@ def test_bad_magic_input():
|
|
|
220
220
|
puremagic.main.perform_magic(None, None, None) # type: ignore[invalid-argument-type]
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
def test_from_extension():
|
|
224
|
+
"""Test from_extension lookup"""
|
|
225
|
+
assert puremagic.from_extension(".pdf") == "application/pdf"
|
|
226
|
+
assert puremagic.from_extension("pdf") == "application/pdf"
|
|
227
|
+
assert puremagic.from_extension("PDF") == "application/pdf"
|
|
228
|
+
assert puremagic.from_extension(".jpg") == "image/jpeg"
|
|
229
|
+
assert puremagic.from_extension("png") == "image/png"
|
|
230
|
+
# Test name mode
|
|
231
|
+
name = puremagic.from_extension(".pdf", mime=False)
|
|
232
|
+
assert name != ""
|
|
233
|
+
# Test unknown extension raises PureError
|
|
234
|
+
with pytest.raises(puremagic.PureError):
|
|
235
|
+
puremagic.from_extension(".xyz_unknown_ext")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_magic_extension():
|
|
239
|
+
"""Test magic_extension returns list of matches"""
|
|
240
|
+
results = puremagic.magic_extension(".pdf")
|
|
241
|
+
assert len(results) >= 1
|
|
242
|
+
assert results[0].mime_type == "application/pdf"
|
|
243
|
+
assert results[0].extension == ".pdf"
|
|
244
|
+
# Check sorted by confidence descending
|
|
245
|
+
for i in range(len(results) - 1):
|
|
246
|
+
assert results[i].confidence >= results[i + 1].confidence
|
|
247
|
+
# Unknown extension returns empty list
|
|
248
|
+
assert puremagic.magic_extension(".xyz_unknown_ext") == []
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_cmd_extension_option():
|
|
252
|
+
"""Test CLI -e option"""
|
|
253
|
+
from puremagic.main import command_line_entry # noqa: PLC0415
|
|
254
|
+
|
|
255
|
+
command_line_entry("-e", "pdf")
|
|
256
|
+
command_line_entry("-e", ".jpg")
|
|
257
|
+
command_line_entry("-e", "pdf", "-v")
|
|
258
|
+
command_line_entry("-e", "xyz_unknown_ext_123")
|
|
259
|
+
|
|
260
|
+
|
|
223
261
|
def test_fake_file():
|
|
224
262
|
results = puremagic.magic_file(filename=Path(LOCAL_DIR, "resources", "fake_file"))
|
|
225
263
|
assert results[0].confidence == 0.5, results
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_riff_wav_mime():
|
|
267
|
+
"""RIFF scanner returns audio/wav (not audio/wave) for WAV files"""
|
|
268
|
+
mime = puremagic.from_file(os.path.join(AUDIO_DIR, "test.wav"), mime=True)
|
|
269
|
+
assert mime == "audio/wav"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_cfbf_doc():
|
|
273
|
+
"""CFBF scanner correctly identifies Word .doc"""
|
|
274
|
+
ext = puremagic.from_file(os.path.join(OFFICE_DIR, "test.doc"))
|
|
275
|
+
assert ext == ".doc"
|
|
276
|
+
mime = puremagic.from_file(os.path.join(OFFICE_DIR, "test.doc"), mime=True)
|
|
277
|
+
assert mime == "application/msword"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_cfbf_ppt():
|
|
281
|
+
"""CFBF scanner correctly identifies PowerPoint .ppt"""
|
|
282
|
+
ext = puremagic.from_file(os.path.join(OFFICE_DIR, "test.ppt"))
|
|
283
|
+
assert ext == ".ppt"
|
|
284
|
+
mime = puremagic.from_file(os.path.join(OFFICE_DIR, "test.ppt"), mime=True)
|
|
285
|
+
assert mime == "application/vnd.ms-powerpoint"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_cfbf_msg():
|
|
289
|
+
"""CFBF scanner correctly identifies Outlook .msg"""
|
|
290
|
+
ext = puremagic.from_file(os.path.join(OFFICE_DIR, "test.msg"))
|
|
291
|
+
assert ext == ".msg"
|
|
292
|
+
mime = puremagic.from_file(os.path.join(OFFICE_DIR, "test.msg"), mime=True)
|
|
293
|
+
assert mime == "application/vnd.ms-outlook"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import puremagic
|
|
2
|
-
from test.common import OFFICE_DIR, SYSTEM_DIR, AUDIO_DIR
|
|
2
|
+
from test.common import IMAGE_DIR, OFFICE_DIR, SYSTEM_DIR, AUDIO_DIR
|
|
3
3
|
from puremagic.scanners import python_scanner, json_scanner, sndhdr_scanner
|
|
4
4
|
|
|
5
5
|
sample_text = b"""Lorem ipsum dolor sit amet, consectetur adipiscing elit,{ending}
|
|
@@ -104,6 +104,25 @@ def test_eml_scanner():
|
|
|
104
104
|
assert results[0].confidence == 1.0
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def test_jpg_without_extension():
|
|
108
|
+
# GH #141: JPEG file without extension should still be identified as image/jpeg
|
|
109
|
+
import struct
|
|
110
|
+
|
|
111
|
+
data = b"\xff\xd8\xff\xe0"
|
|
112
|
+
data += struct.pack(">H", 16)
|
|
113
|
+
data += b"JFIF\x00\x01\x01\x00"
|
|
114
|
+
data += struct.pack(">HH", 1, 1)
|
|
115
|
+
data += b"\x00\x00\xff\xd9"
|
|
116
|
+
|
|
117
|
+
no_ext_file = IMAGE_DIR / "test_jpeg_no_ext"
|
|
118
|
+
no_ext_file.write_bytes(data)
|
|
119
|
+
try:
|
|
120
|
+
result = puremagic.from_file(no_ext_file, mime=True)
|
|
121
|
+
assert result == "image/jpeg", f"Expected image/jpeg, got {result}"
|
|
122
|
+
finally:
|
|
123
|
+
no_ext_file.unlink()
|
|
124
|
+
|
|
125
|
+
|
|
107
126
|
def test_sndhdr_scanner():
|
|
108
127
|
# Test the sndhdr scanner with sndr file
|
|
109
128
|
sndr_file = AUDIO_DIR / "test.sndr"
|
|
@@ -92,6 +92,15 @@ wheels = [
|
|
|
92
92
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
|
93
93
|
]
|
|
94
94
|
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "codespell"
|
|
97
|
+
version = "2.4.2"
|
|
98
|
+
source = { registry = "https://pypi.org/simple" }
|
|
99
|
+
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
|
|
100
|
+
wheels = [
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
|
|
102
|
+
]
|
|
103
|
+
|
|
95
104
|
[[package]]
|
|
96
105
|
name = "colorama"
|
|
97
106
|
version = "0.4.6"
|
|
@@ -495,6 +504,7 @@ source = { editable = "." }
|
|
|
495
504
|
|
|
496
505
|
[package.dev-dependencies]
|
|
497
506
|
dev = [
|
|
507
|
+
{ name = "codespell" },
|
|
498
508
|
{ name = "coverage" },
|
|
499
509
|
{ name = "poethepoet" },
|
|
500
510
|
{ name = "pre-commit" },
|
|
@@ -511,14 +521,15 @@ dev = [
|
|
|
511
521
|
|
|
512
522
|
[package.metadata.requires-dev]
|
|
513
523
|
dev = [
|
|
524
|
+
{ name = "codespell", specifier = ">=2.4.2" },
|
|
514
525
|
{ name = "coverage", specifier = ">=7.13.4" },
|
|
515
526
|
{ name = "poethepoet", specifier = ">=0.42.1" },
|
|
516
527
|
{ name = "pre-commit", specifier = ">=4.5.1" },
|
|
517
528
|
{ name = "pytest", specifier = ">=9.0.2" },
|
|
518
|
-
{ name = "pytest-cov", specifier = ">=7
|
|
529
|
+
{ name = "pytest-cov", specifier = ">=7" },
|
|
519
530
|
{ name = "ruff", specifier = ">=0.15.5" },
|
|
520
|
-
{ name = "setuptools", specifier = ">=82
|
|
521
|
-
{ name = "twine", specifier = ">=6.2
|
|
531
|
+
{ name = "setuptools", specifier = ">=82" },
|
|
532
|
+
{ name = "twine", specifier = ">=6.2" },
|
|
522
533
|
{ name = "ty", specifier = ">=0.0.21" },
|
|
523
534
|
{ name = "wheel", specifier = ">=0.46.3" },
|
|
524
535
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3
RENAMED
|
File without changes
|
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3
RENAMED
|
File without changes
|
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3
RENAMED
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3
RENAMED
|
File without changes
|
{puremagic-2.0.2 → puremagic-2.1.1}/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|