diff --git a/.gitignore b/.gitignore index b096391..5588cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,7 @@ media static .idea/ + +# Ignore VS Code related files and folders +.vscode +*.code-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d8783..b255a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog Version 2.0.0 ------------- -- Adding deep scan for improved accuracy #102 #94 #70 #69 #12 #3 +- Adding deep scan for improved accuracy #102 #94 #85 #70 #69 #12 #3 - Changing to full semantic versioning to be able to denote bugfixes vs minor features - Changing to use uv instead of requirements - Removing support for python 3.7, 3.8, 3.9, 3.10 and 3.11 please stick to 1.x release chain to support older versions diff --git a/puremagic/magic_data.json b/puremagic/magic_data.json index e54c31b..2e02c64 100644 --- a/puremagic/magic_data.json +++ b/puremagic/magic_data.json @@ -88,7 +88,7 @@ ["38535658", 8, ".iff", "audio/x-8svx", "IFF 8-Bit Sampled Voice"], ["4143424d", 8, ".iff", "application/x-iff", "Amiga Contiguous Bitmap"], ["414e424d", 8, ".iff", "application/x-iff", "IFF Animated Bitmap"], - ["414e494d", 8, ".iff", "application/x-iff", " IFF CEL Animation"], + ["414e494d", 8, ".iff", "application/x-iff", "IFF CEL Animation"], ["46415858", 8, ".iff", "application/x-iff", "IFF Facsimile Image"], ["46545854", 8, ".iff", "application/x-iff", "IFF Formatted Text"], ["534d5553", 8, ".iff", "application/x-iff", "IFF Facsimile Image"], @@ -170,242 +170,256 @@ ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) audio file"] ], "4944330200": [ - ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["434E54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["434F4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["435241", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["43524D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["455443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["455155", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["47454F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["49504C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["4C4E4B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["4D4349", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["4D4C4C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["504F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["524556", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["525641", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["534C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["535443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54414C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544250", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54434D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54434F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544441", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544459", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54454E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544654", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54494D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544B45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544C41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544C45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544D54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544F41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544F46", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544F4C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544F52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["544F54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545031", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545032", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545033", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545034", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545041", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545042", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545244", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["54524B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545349", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["545945", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["554649", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["554C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["574146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["574152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["574153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["57434D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["574350", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["575042", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"], - ["575858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.2.0 audio file"] + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["41454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["434e54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["434f4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["435241", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["43524d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["455443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["455155", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["47454f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["4c4e4b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["4d4349", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["4d4c4c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["504f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["524556", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["525641", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["534c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["535443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54414c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544250", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54434d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54434f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544441", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544459", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544654", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54494d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544b45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544c41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544c45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544d54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544f41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544f46", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544f4c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544f52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["544f54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545031", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545032", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545033", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545034", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545041", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545042", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545244", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["54524b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["545945", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["554649", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["554c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["574146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["574152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["574153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["57434d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["574350", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["575042", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["575858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["574952", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"], + ["55494e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.2.0 audio file"] ], "4944330300": [ - ["41454E43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["41504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["41535049", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["434F4D4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["434F4D52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["454E4352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["45515532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["4554434F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["47454F42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["47524944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["4C494E4B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["4D434449", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["4D4C4C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["4F574E45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["50524956", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["50434E54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["504F504D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["504F5353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["52425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["52564132", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["52565242", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5345454B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5349474E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["53594C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["53595443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54414C42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5442504D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54434F4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54434F4E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54434F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5444454E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54444C59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54444F52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54445243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5444524C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54445447", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54454E43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54455854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54464C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5449504C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54495431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54495432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54495433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544B4559", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544C414E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544C454E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544D434C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544D4544", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544D4F4F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544F414C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544F464E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544F4C59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544F5045", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544F574E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54504531", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54504532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54504533", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54504534", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54504F53", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5450524F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5452434B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5452534E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["5452534F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54534F41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54534F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54534F54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54535243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54535345", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54535354", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["54585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["55464944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["55534552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["55534C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["57434F4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["57434F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["574F4146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["574F4152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["574F4153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["574F5253", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["57504159", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["57505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["57585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"], - ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.3.0 audio file"] + ["41454e43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["41504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["41535049", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["434f4d4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["434f4d52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["454e4352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["45515532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["4554434f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["47454f42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["47524944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["4c494e4b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["4d434449", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["4d4c4c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["4f574e45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["50524956", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["50434e54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["504f504d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["504f5353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["52425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["52564132", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["52565242", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5345454b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5349474e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["53594c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["53595443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["55464944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["55534552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["55534c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["57434f4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["57434f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574f4146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574f4152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574f4153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574f5253", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["57504159", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["57505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["57585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54594552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54444154", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54494d45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f5259", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54414c42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5442504d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54434f4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54434f4e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54434f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5444454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54444c59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54444f52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54445243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5444524c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54445447", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54454e43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54455854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54464c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5449504c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54495431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54495432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54495433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544b4559", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544c414e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544c454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544d434c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544d4544", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544d4f4f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f414c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f464e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f4c59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f5045", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544f574e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54504531", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54504532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54504533", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54504534", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54504f53", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5450524f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5452434b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5452534e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["5452534f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54534f41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54534f43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54534f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54534f54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54535243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54535345", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54535354", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["54585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["574952", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["575959", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"], + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.3.0 audio file"] ], "4944330400": [ - ["41454E43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["41504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["41535049", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["434F4D4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["434F4D52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["454E4352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["45515532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["4554434F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["47454F42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["47524944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["4C494E4B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["4D434449", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["4D4C4C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["4F574E45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["50524956", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["50434E54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["504F504D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["504F5353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["52425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["52564132", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["52565242", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5345454B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5349474E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["53594C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["53595443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54414C42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5442504D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54434F4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54434F4E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54434F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5444454E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54444C59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54444F52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54445243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5444524C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54445447", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54454E43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54455854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54464C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5449504C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54495431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54495432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54495433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544B4559", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544C414E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544C454E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544D434C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544D4544", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544D4F4F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544F414C", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544F464E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544F4C59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544F5045", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544F574E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54504531", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54504532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54504533", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54504534", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54504F53", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5450524F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5452434B", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5452534E", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["5452534F", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54534F41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54534F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54534F54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54535243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54535345", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54535354", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["54585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["55464944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["55534552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["55534C54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["57434F4D", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["57434F50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["574F4146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["574F4152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["574F4153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["574F5253", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["57504159", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["57505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["57585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"], - ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) ID3v2.4.0 audio file"] + ["41454e43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["41504943", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["41535049", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["434f4d4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["434f4d52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["454e4352", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["45515532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["4554434f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["47454f42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["47524944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["4c494e4b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["4d434449", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["4d4c4c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["4f574e45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["50524956", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["50434e54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["504f504d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["504f5353", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["52425546", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["52564132", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["52565242", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5345454b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5349474e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["53594c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["53595443", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["55464944", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["55534552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["55534c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["57434f4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["57434f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["574f4146", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["574f4152", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["574f4153", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["574f5253", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["57504159", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["57505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["57585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54594552", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54444154", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54494d45", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f5259", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54414c42", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5442504d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54434f4d", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54434f4e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54434f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5444454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54444c59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54444f52", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54445243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5444524c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54445447", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54454e43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54455854", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54464c54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5449504c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54495431", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54495432", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54495433", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544b4559", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544c414e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544c454e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544d434c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544d4544", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544d4f4f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f414c", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f464e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f4c59", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f5045", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544f574e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54504531", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54504532", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54504533", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54504534", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54504f53", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5450524f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54505542", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5452434b", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5452534e", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["5452534f", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54534f41", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54534f43", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54534f50", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54534f54", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54535243", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54535345", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54535354", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["54585858", 10, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"], + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) ID3v2.4.0 audio file"] ], "01da" : [ ["00010001", 2, ".rgb", "image/x-rgb", "Silicon Graphics RGB Bitmap (Uncompressed, 1bpc, single row)"], @@ -537,7 +551,7 @@ ["435202", 8, ".cr2", "", "Canon Camera RAW 2 image"] ], "424f4f4b4d4f4249" : [ - ["e98e0d0a", -4, ".mobi", "application/x-mobipocket-ebook", "Mobipocket eBook file"], + ["e98e0d0a", -4, ".mobi", "application/x-mobipocket-ebook", "Mobipocket eBook file"], ["e98e0d0a", -4, ".azw", "application/vnd.amazon.mobi8-ebook", "Amazon Kindle eBook file"], ["434f4e54424f554e44415259e98e0d0a", -16, ".azw3", "application/vnd.amazon.mobi8-ebook", "Amazon Kindle Format 8 eBook file (KF8 Dual MOBI/EPUB Format)"] ], @@ -585,6 +599,126 @@ ["05", 3, ".adf", "application/x-amiga-disk-format", "Amiga disk image (FFS International and Directory Cache)"], ["06", 3, ".adf", "application/x-amiga-disk-format", "Amiga disk image (OFS Long Filename)"], ["07", 3, ".adf", "application/x-amiga-disk-format", "Amiga disk image (FFS Long Filename)"] + ], + "ffd0": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer Reserved/Illegal file"] + ], + "ffd1": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer Reserved/Illegal file"] + ], + "ffd6": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffd7": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffda": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffdb": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffde": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffdf": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffe0": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer Reserved/Illegal file"] + ], + "ffe1": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer Reserved/Illegal file"] + ], + "ffe2": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffe3": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"] + ], + "ffe4": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"] + ], + "ffe5": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"] + ], + "ffe6": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"] + ], + "ffe7": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"] + ], + "ffe8": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffe9": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffea": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffeb": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffec": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffed": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"] + ], + "ffee": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-2 Audio Layer I (MP1) file"] + ], + "ffef": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-2 Audio Layer I (MP1) file"] + ], + "fff0": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer Reserved/Illegal file"] + ], + "fff1": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer Reserved/Illegal file"] + ], + "fff2": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff3": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff4": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff5": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff6": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff7": [ + ["544147", -128, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"] + ], + "fff8": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-1 Audio Layer II (MP2) file"] + ], + "fff9": [ + ["544147", -128, ".mp2", "audio/mpeg", "MPEG-1 Audio Layer II (MP2) file"] + ], + "fffa": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] + ], + "fffb": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] + ], + "fffc": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] + ], + "fffd": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] + ], + "fffe": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] + ], + "ffff": [ + ["544147", -128, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"] ] }, "footers": [ @@ -598,6 +732,8 @@ ["4e455235", -12, ".nrg", "", "Nero Disk Image (Version 2)"] ], "headers": [ + ["0000", 0, ".sndr", "audio/x-sndr", "Macintosh SNDR Resource"], + ["437265617469766520566f6963652046696c651a", 0, ".voc", "audio/x-voc", "Creative Voice File"], ["595556344d504547",0, ".y4m", "video/x-yuv4mpeg", "YUV4MPEG2 video file"], ["3c68746d6c", 0, ".html", "text/html", "HTML File"], ["424c5545", 0, ".bvr", "", "Blue Iris Video File"], @@ -750,7 +886,6 @@ ["4d4d4d44", 0, ".smaf", "application/x-smaf", "SMAF audio"], ["50534944", 0, ".psid", "audio/prs.sid", "Commodore 64 audio"], ["664c6143", 0, ".flac", "audio/flac", "FLAC audio"], - ["fffb", 0, ".mpga", "audio/mpeg", "MP3 audio"], ["234558544d3355", 0, ".m3u8", "application/vnd.apple.mpegurl", "HTTP Live Streaming playlist"], ["2521", 0, ".epsf", "image/x-eps", "EPS image"], ["5c3030342521", 0, ".epsf", "image/x-eps", "EPS image"], @@ -797,10 +932,50 @@ ["2e524543", 0, ".ivr", "i-world/i-vrml", "RealPlayer video file"], ["6d6f6f76", 4, ".mov", "video/quicktime", "QuickTime movie file"], ["3026b2758e66cf11a6d900aa0062ce6c", 0, ".wma", "audio/x-ms-wma", "Microsoft Windows Media Audio file"], - ["494433", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 (MP3) audio file"], - ["4944330200", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 ID3v2.2.0 (MP3) audio file"], - ["4944330300", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 ID3v2.3.0 (MP3) audio file"], - ["4944330400", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer 3 ID3v2.4.0 (MP3) audio file"], + ["494433", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) audio file"], + ["4944330200", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III ID3v2.2.0 (MP3) audio file"], + ["4944330300", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III ID3v2.3.0 (MP3) audio file"], + ["4944330400", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III ID3v2.4.0 (MP3) audio file"], + ["ffd0", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer Reserved/Illegal"], + ["ffd1", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer Reserved/Illegal"], + ["ffd6", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffd7", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffda", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffdb", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffde", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffdf", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffe0", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer Reserved/Illegal"], + ["ffe1", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer Reserved/Illegal"], + ["ffe2", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffe3", 0, ".mp3", "audio/mpeg", "MPEG-2.5 Audio Layer III (MP3) file"], + ["ffe4", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"], + ["ffe5", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"], + ["ffe6", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"], + ["ffe7", 0, ".mp3", "audio/mpeg", "MPEG-2 Audio Layer III (MP3) file"], + ["ffe8", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffe9", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffea", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffeb", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffec", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffed", 0, ".mp2", "audio/mpeg", "MPEG-2 Audio Layer II (MP2) file"], + ["ffee", 0, ".mp1", "audio/mpeg", "MPEG-2 Audio Layer I (MP1) file"], + ["ffef", 0, ".mp1", "audio/mpeg", "MPEG-2 Audio Layer I (MP1) file"], + ["fff0", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer Reserved/Illegal"], + ["fff1", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer Reserved/Illegal"], + ["fff2", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff3", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff4", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff5", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff6", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff7", 0, ".mp3", "audio/mpeg", "MPEG-1 Audio Layer III (MP3) file"], + ["fff8", 0, ".mp2", "audio/mpeg", "MPEG-1 Audio Layer II (MP2) file"], + ["fff9", 0, ".mp2", "audio/mpeg", "MPEG-1 Audio Layer II (MP2) file"], + ["fffa", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], + ["fffb", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], + ["fffc", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], + ["fffd", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], + ["fffe", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], + ["ffff", 0, ".mp1", "audio/mpeg", "MPEG-1 Audio Layer I (MP1) file"], ["4f67675300020000000000000000", 0, ".ogg", "application/ogg", "Ogg Vorbis audio file"], ["57415645666d7420", 8, ".wav", "audio/x-wav", "Windows audio file "], ["464f524d", 0, ".aif", "audio/x-aiff", "Audio Interchange File"], @@ -1745,9 +1920,6 @@ ["0a0d0d0a", 0, ".pcapng", "application/octet-stream", "pcapng capture file"], ["05000000", 0, "", "", "INFO2 Windows recycle bin"], ["34cdb2a1", 0, "", "", "Tcpdump capture file"], - ["fffe0000", 0, "", "", "UTF-32|UCS-4 file"], - ["efbbbf", 0, "", "", "UTF8 file"], - ["feff", 0, "", "", "UTF-16|UCS-2 file"], ["6f3c", 0, "", "", "SMS text (SIM)"], ["aced", 0, "", "", "Java serialization data"], [ @@ -1887,7 +2059,7 @@ ["424c49323233", 0, ".bin", "application/x-binary", "Speedtouch router firmware"], ["424c49323233", 0, ".bli", "application/octet-stream", "Speedtouch router firmware"], ["424c49323233", 0, ".rbi", "application/octet-stream", "Speedtouch router firmware"], - ["49443303000000", 0, ".koz", "", "Sprint Music Store audio"], + ["49443303000000", 0, ".koz", "audio/vnd.audiokoz", "Sprint Music Store audio"], ["5350464900", 0, ".spf", "", "StorageCraft ShadownProtect backup file"], ["4c413a", 0, ".dst", "", "Tajima emboridery"], ["4d435720546563686e6f676f6c696573", 0, ".mte", "", "TargetExpress target file"], @@ -2185,6 +2357,7 @@ ["5244534b", 0, ".hdf", "", "Amiga Harddisk image"], ["504653", 0, ".hdf", "", "Amiga Harddisk image (Professional Filesystem 3)"], ["504453", 0, ".hdf", "", "Amiga Harddisk image (Professional Filesystem 3)"], - ["534653", 0, ".hdf", "", "Amiga Harddisk image (Smart File System)"] + ["534653", 0, ".hdf", "", "Amiga Harddisk image (Smart File System)"], + ["d0cf11e0a1b11ae1", 0, ".msg", "application/vnd.ms-outlook", "Outlook 97-2003 Item File"] ] } diff --git a/puremagic/main.py b/puremagic/main.py index 0dfd428..f55bb7c 100644 --- a/puremagic/main.py +++ b/puremagic/main.py @@ -22,10 +22,18 @@ import puremagic if os.getenv("PUREMAGIC_DEEPSCAN") != "0": - from puremagic.scanners import zip_scanner, pdf_scanner, text_scanner, json_scanner, python_scanner + from puremagic.scanners import ( + zip_scanner, + pdf_scanner, + text_scanner, + json_scanner, + python_scanner, + sndhdr_scanner, + mpeg_audio_scanner, + ) __author__ = "Chris Griffith" -__version__ = "2.0.0b4" +__version__ = "2.0.0b5" __all__ = [ "magic_file", "magic_string", @@ -35,17 +43,10 @@ "from_stream", "ext_from_filename", "PureError", - "magic_footer_array", - "magic_header_array", - "multi_part_dict", - "what", "PureMagic", "PureMagicWithConfidence", ] -# Convert puremagic extensions to imghdr extensions -imghdr_exts = {"dib": "bmp", "jfif": "jpeg", "jpg": "jpeg", "rst": "rast", "sun": "rast", "tif": "tiff"} - here = os.path.abspath(os.path.dirname(__file__)) PureMagic = namedtuple( @@ -75,22 +76,26 @@ class PureError(LookupError): """Do not have that type of file in our databanks""" -def _magic_data( +class PureValueError(ValueError): + """Invalid input""" + + +def magic_data( filename: os.PathLike | str = os.path.join(here, "magic_data.json"), ) -> tuple[list[PureMagic], list[PureMagic], list[PureMagic], dict[bytes, list[PureMagic]]]: """Read the magic file""" with open(filename, encoding="utf-8") as f: data = json.load(f) - headers = sorted((_create_puremagic(x) for x in data["headers"]), key=lambda x: x.byte_match) - footers = sorted((_create_puremagic(x) for x in data["footers"]), key=lambda x: x.byte_match) - extensions = [_create_puremagic(x) for x in data["extension_only"]] + headers = sorted((create_puremagic(x) for x in data["headers"]), key=lambda x: x.byte_match) + footers = sorted((create_puremagic(x) for x in data["footers"]), key=lambda x: x.byte_match) + extensions = [create_puremagic(x) for x in data["extension_only"]] multi_part_extensions = {} for file_match, option_list in data["multi-part"].items(): - multi_part_extensions[unhexlify(file_match.encode("ascii"))] = [_create_puremagic(x) for x in option_list] + multi_part_extensions[unhexlify(file_match.encode("ascii"))] = [create_puremagic(x) for x in option_list] return headers, footers, extensions, multi_part_extensions -def _create_puremagic(x: list) -> PureMagic: +def create_puremagic(x: list) -> PureMagic: return PureMagic( byte_match=unhexlify(x[0].encode("ascii")), offset=x[1], @@ -100,10 +105,10 @@ def _create_puremagic(x: list) -> PureMagic: ) -magic_header_array, magic_footer_array, extension_only_array, multi_part_dict = _magic_data() +magic_header_array, magic_footer_array, extension_only_array, multi_part_dict = magic_data() -def _max_lengths() -> tuple[int, int]: +def get_max_lengths() -> tuple[int, int]: """The length of the largest magic string + its offset""" max_header_length = max([len(x.byte_match) + x.offset for x in magic_header_array]) max_footer_length = max([len(x.byte_match) + abs(x.offset) for x in magic_footer_array]) @@ -118,10 +123,10 @@ def _max_lengths() -> tuple[int, int]: return max_header_length, max_footer_length -max_head, max_foot = _max_lengths() +max_head, max_foot = get_max_lengths() -def _confidence(matches, ext=None) -> list[PureMagicWithConfidence]: +def determine_confidence(matches, ext=None) -> list[PureMagicWithConfidence]: """Rough confidence based on string length and file extension""" results = [] for match in matches: @@ -140,7 +145,7 @@ def _confidence(matches, ext=None) -> list[PureMagicWithConfidence]: return sorted(results, key=lambda x: (x.confidence, len(x.byte_match)), reverse=True) -def _identify_all(header: bytes, footer: bytes, ext=None) -> list[PureMagicWithConfidence]: +def identify_all(header: bytes, footer: bytes, ext=None) -> list[PureMagicWithConfidence]: """Attempt to identify 'data' by its magic numbers""" # Capture the length of the data @@ -194,16 +199,16 @@ def _identify_all(header: bytes, footer: bytes, ext=None) -> list[PureMagicWithC ) matches.extend(list(new_matches)) - return _confidence(matches, ext) + return determine_confidence(matches, ext) -def _magic(header: bytes, footer: bytes, mime: bool, ext=None, filename=None) -> str: +def perform_magic(header: bytes, footer: bytes, mime: bool, ext=None, filename=None) -> str: """Discover what type of file it is based on the incoming string""" if not header: - raise ValueError("Input was empty") - infos = _identify_all(header, footer, ext) + raise PureValueError("Input was empty") + infos = identify_all(header, footer, ext) if filename and os.getenv("PUREMAGIC_DEEPSCAN") != "0": - results = _run_deep_scan(infos, filename, header, footer, raise_on_none=True) + results = run_deep_scan(infos, filename, header, footer, raise_on_none=True) if results: if results[0].extension == "": raise PureError("Could not identify file") @@ -218,7 +223,7 @@ def _magic(header: bytes, footer: bytes, mime: bool, ext=None, filename=None) -> return info.extension if not isinstance(info.extension, list) else info[0].extension -def _file_details(filename: os.PathLike | str) -> tuple[bytes, bytes]: +def file_details(filename: os.PathLike | str) -> tuple[bytes, bytes]: """Grab the start and end of the file""" if not os.path.isfile(filename): raise PureError("Not a regular file") @@ -232,12 +237,12 @@ def _file_details(filename: os.PathLike | str) -> tuple[bytes, bytes]: return head, foot -def _string_details(string): +def string_details(string): """Grab the start and end of the string""" return string[:max_head], string[-max_foot:] -def _stream_details(stream): +def stream_details(stream): """Grab the start and end of the stream""" head = stream.read(max_head) try: @@ -281,8 +286,8 @@ def from_file(filename: os.PathLike | str, mime: bool = False) -> str: :return: guessed extension or mime """ - head, foot = _file_details(filename) - return _magic(head, foot, mime, ext_from_filename(filename), filename=filename) + head, foot = file_details(filename) + return perform_magic(head, foot, mime, ext_from_filename(filename), filename=filename) def from_string(string: str | bytes, mime: bool = False, filename: os.PathLike | str | None = None) -> str: @@ -298,9 +303,9 @@ def from_string(string: str | bytes, mime: bool = False, filename: os.PathLike | """ if isinstance(string, str): string = string.encode("utf-8") - head, foot = _string_details(string) + head, foot = string_details(string) ext = ext_from_filename(filename) if filename else None - return _magic(head, foot, mime, ext) + return perform_magic(head, foot, mime, ext) def from_stream(stream, mime: bool = False, filename: os.PathLike | str | None = None) -> str: @@ -314,9 +319,9 @@ def from_stream(stream, mime: bool = False, filename: os.PathLike | str | None = :param filename: original filename :return: guessed extension or mime """ - head, foot = _stream_details(stream) + head, foot = stream_details(stream) ext = ext_from_filename(filename) if filename else None - return _magic(head, foot, mime, ext) + return perform_magic(head, foot, mime, ext) def magic_file(filename: os.PathLike | str) -> list[PureMagicWithConfidence]: @@ -327,16 +332,16 @@ def magic_file(filename: os.PathLike | str) -> list[PureMagicWithConfidence]: :param filename: path to file :return: list of possible matches, highest confidence first """ - head, foot = _file_details(filename) + head, foot = file_details(filename) if not head: - raise ValueError("Input was empty") + raise PureValueError("Input was empty") try: - info = _identify_all(head, foot, ext_from_filename(filename)) + info = identify_all(head, foot, ext_from_filename(filename)) except PureError: info = [] info.sort(key=lambda x: x.confidence, reverse=True) if os.getenv("PUREMAGIC_DEEPSCAN") != "0": - return _run_deep_scan(info, filename, head, foot, raise_on_none=False) + return run_deep_scan(info, filename, head, foot, raise_on_none=False) return info @@ -351,10 +356,10 @@ def magic_string(string, filename: os.PathLike | str | None = None) -> list[Pure :return: list of possible matches, highest confidence first """ if not string: - raise ValueError("Input was empty") - head, foot = _string_details(string) + raise PureValueError("Input was empty") + head, foot = string_details(string) ext = ext_from_filename(filename) if filename else None - info = _identify_all(head, foot, ext) + info = identify_all(head, foot, ext) info.sort(key=lambda x: x.confidence, reverse=True) return info @@ -371,20 +376,21 @@ def magic_stream( :param filename: original filename :return: list of possible matches, highest confidence first """ - head, foot = _stream_details(stream) + head, foot = stream_details(stream) if not head: - raise ValueError("Input was empty") + raise PureValueError("Input was empty") ext = ext_from_filename(filename) if filename else None - info = _identify_all(head, foot, ext) + info = identify_all(head, foot, ext) info.sort(key=lambda x: x.confidence, reverse=True) return info -def _single_deep_scan( +def single_deep_scan( bytes_match: bytes | bytearray | None, filename: os.PathLike | str, head=None, foot=None, + confidence=0, ): if os.getenv("PUREMAGIC_DEEPSCAN") == "0": return None @@ -395,8 +401,17 @@ def _single_deep_scan( return zip_scanner.main(filename, head, foot) case pdf_scanner.match_bytes: return pdf_scanner.main(filename, head, foot) - - # First match wins, so text_scanner should always be last + case sndhdr_scanner.hcom_match_bytes | sndhdr_scanner.fssd_match_bytes | sndhdr_scanner.sndr_match_bytes: + # sndr is a loose confidence and other results may be better + result = sndhdr_scanner.main(filename, head, foot) + if result and result.confidence > confidence: + return result + case mpeg_bytes if mpeg_bytes in mpeg_audio_scanner.mpeg_audio_signatures: + result = mpeg_audio_scanner.main(filename, head, foot) + if result and result.confidence > confidence: + return result + + # The first match wins for scanner in (pdf_scanner, python_scanner, json_scanner): result = scanner.main(filename, head, foot) if result: @@ -404,7 +419,7 @@ def _single_deep_scan( return None -def _catch_all_deep_scan( +def catch_all_deep_scan( filename: os.PathLike | str, head=None, foot=None, @@ -416,7 +431,7 @@ def _catch_all_deep_scan( return text_scanner.main(filename, head, foot) -def _run_deep_scan( +def run_deep_scan( matches: list[PureMagicWithConfidence], filename: os.PathLike | str, head=None, @@ -425,7 +440,7 @@ def _run_deep_scan( ): if not matches or matches[0].byte_match == b"": try: - result = _single_deep_scan(None, filename, head, foot) + result = single_deep_scan(None, filename, head, foot) except Exception: pass else: @@ -441,9 +456,9 @@ def _run_deep_scan( ) ] try: - result = _catch_all_deep_scan(filename, head, foot) + result = catch_all_deep_scan(filename, head, foot) except Exception: - pass + raise else: if result: return [result] @@ -453,7 +468,7 @@ def _run_deep_scan( for pure_magic_match in matches: # noinspection PyBroadException try: - result = _single_deep_scan(pure_magic_match.byte_match, filename, head, foot) + result = single_deep_scan(pure_magic_match.byte_match, filename, head, foot, pure_magic_match.confidence) except Exception: continue if result: @@ -488,25 +503,35 @@ def command_line_entry(*args): help="Return the mime type instead of file type", ) parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="Print verbose output") - parser.add_argument("files", nargs="+") + parser.add_argument("files", nargs="+", type=Path) parser.add_argument("--version", action="version", version=puremagic.__version__) args = parser.parse_args(args if args else sys.argv[1:]) for fn in args.files: - if not os.path.exists(fn): + if not fn.exists(): print(f"File '{fn}' does not exist!") continue - try: - print(f"'{fn}' : {from_file(fn, args.mime)}") - except PureError: - print(f"'{fn}' : could not be Identified") - continue + if fn.is_dir(): + for file in fn.iterdir(): + if not file.is_file(): + continue + try: + print(f"'{file}' : {from_file(file, args.mime)}") + except (PureError, PureValueError): + print(f"'{file}' : could not be Identified") + continue + else: + try: + print(f"'{fn}' : {from_file(fn, args.mime)}") + except (PureError, PureValueError): + print(f"'{fn}' : could not be Identified") + continue if args.verbose: matches = magic_file(fn) print(f"Total Possible Matches: {len(matches)}") for i, result in enumerate(matches): if i == 0: - print("\n\tBest Match") + print("\n\tDeepscan Match" if int(result.confidence == 1) else "\n\tBest Match") else: print(f"\tAlternative Match #{i}") print(f"\tName: {result.name}") @@ -517,66 +542,5 @@ def command_line_entry(*args): print(f"\tOffset: {result.offset}\n") -imghdr_bug_for_bug = { # Special cases where imghdr is probably incorrect. - b"______Exif": "jpeg", - b"______JFIF": "jpeg", - b"II": "tiff", - b"II\\x2a\\x00": "tiff", - b"MM": "tiff", - b"MM\\x00\\x2a": "tiff", -} - - -def what(file: os.PathLike | str | None, h: bytes | None = None, imghdr_strict: bool = True) -> str | None: - """A drop-in replacement for `imghdr.what()` which was removed from the standard - library in Python 3.13. - - Usage: - ```python - # Replace... - from imghdr import what - - # with... - from puremagic import what - - # --- - # Or replace... - import imghdr - - ext = imghdr.what(...) - # with... - import puremagic - - ext = puremagic.what(...) - ``` - imghdr documentation: https://docs.python.org/3.12/library/imghdr.html - imghdr source code: https://github.com/python/cpython/blob/3.12/Lib/imghdr.py - - imghdr_strict enables bug-for-bug compatibility between imghdr.what() and puremagic.what() when the imghdr returns - a match but puremagic returns None. We believe that imghdr is delivering a "false positive" in each of these - scenarios, but we want puremagic.what()'s default behavior to match imghdr.what()'s false positives so we do not - break existing applications. - - If imghdr_strict is True (the default) then a lookup will be done to deliver a matching result on all known false - positives. If imghdr_strict is False then puremagic's algorithms will determine the image type. True is more - compatible while False is more correct. - - NOTE: This compatibility effort only deals false positives, and we are not interested to track the opposite - situation where puremagic's deliver a match while imghdr would have returned None. Also, puremagic.what() can - recognize many more file types than the twelve image file types that imghdr focused on. - """ - if isinstance(h, str): - raise TypeError("h must be bytes, not str. Consider using bytes.fromhex(h)") - if h and imghdr_strict: - ext = imghdr_bug_for_bug.get(h) - if ext: - return ext - try: - ext = (from_string(h) if h else from_file(file or "")).lstrip(".") - except PureError: - return None # imghdr.what() returns None if it cannot find a match. - return imghdr_exts.get(ext, ext) - - if __name__ == "__main__": # pragma: no cover command_line_entry() diff --git a/puremagic/scanners/json_scanner.py b/puremagic/scanners/json_scanner.py index 04ef3b0..58953e5 100644 --- a/puremagic/scanners/json_scanner.py +++ b/puremagic/scanners/json_scanner.py @@ -7,7 +7,7 @@ def main(file_path: os.PathLike | str, head: bytes, foot: bytes) -> Match | None: - if not (head.strip().startswith(b"{") and foot.strip().endswith(b"}")): + if not (head.strip().startswith(b"{") and foot.strip().endswith(b"}")) and not (head.strip().startswith(b"[") and foot.strip().endswith(b"]")): return None try: with open(file_path, "rb") as file: diff --git a/puremagic/scanners/mpeg_audio_scanner.py b/puremagic/scanners/mpeg_audio_scanner.py new file mode 100644 index 0000000..98d3ca5 --- /dev/null +++ b/puremagic/scanners/mpeg_audio_scanner.py @@ -0,0 +1,1169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# cSpell:disable + +""" +MPEG Audio Deep Scanner (.mp1, .mp2, .mp3). + +This performs a deepscan to confirm if a file is a bonafide MP3 +A successful match is only returned if the main MPEG audio data stream can be decoded correctly. +*AND* if present an ID3v2 (which needs decoding to find the audio afterwards). + +The scanner quickly pulls out all the crucial stream details: + * MPEG Version and Layer (MPEG 1/2/2.5 - Layer I/II/III) + * Sample Rate, Bit Rate, Stereo/Mono + * Detects and checks LAME Xing/Info and Fraunhofer VBRI frames + * Detects CBR vs. VBR encoding through frame analysis (does not rely on above tags) + * Frame analysis also confirms validity of MP3 stream + * Detects and checks ID3v2, ID3v1, APE v1/v2, Lyrics3 v1/v2, ID3v1.2 EXT, ID3v1 TAG+ and 3DI tags + +Note on Tags: + * End-of-file metadata tags (like ID3v1 or APE) are checked purely for informational + purposes and do not influence the file's pass/fail status. You may have a perfectly valid audio stream, + but have horrifically malformed tags at the end, if the audio data is valid we pass the file. + * LAME Xing/Info and Fraunhofer VBRI tags are checked for validity and are used for informational + purposes only, they should not affect pass/fail status unless they are so badly malformed + they cause the main audio decoder to fail finding valid frames. + +""" + +import os +import struct +from typing import IO, Any, Dict, List, Optional + +from puremagic.scanners.helpers import Match + +mpeg_audio_signatures = [ + # These are all the valid signatures for raw MPEG Audio streams (Layers I, II, III), + # or those starting with a ID3v2 tag. You may spot some duplicates, this is fine, + # MPEG audio is full of little joys like this where the same header may mean + # different things when the stream is actually decoded. + b"ID3", # ID3 Tag (Metadata header, often precedes the audio stream) + # ILLEGAL/RESERVED COMBINATIONS (Layer Bits = 00) + # These represent reserved/illegal layer combinations for the three valid versions. + b"\xff\xf0", # MPEG-1, Layer Reserved, Protected (CRC used) + b"\xff\xf1", # MPEG-1, Layer Reserved, No Protection (CRC not used) + b"\xff\xe0", # MPEG-2, Layer Reserved, Protected (CRC used) + b"\xff\xe1", # MPEG-2, Layer Reserved, No Protection (CRC not used) + b"\xff\xd0", # MPEG-2.5, Layer Reserved, Protected (CRC used) + b"\xff\xd1", # MPEG-2.5, Layer Reserved, No Protection (CRC not used) + b"\xff\xf2", # MPEG-1, Layer III (MP3), No Protection (CRC not used) + # MPEG-1 HEADERS (Version Bits = 11) + # Layer III (MP3) - Layer Bits = 01 + b"\xff\xfb", # MPEG-1, Layer III (MP3), Protected (CRC used) + b"\xff\xfa", # MPEG-1, Layer III (MP3), Protected (CRC used) + b"\xff\xf7", # MPEG-1, Layer III (MP3), No Protection (CRC not used) + b"\xff\xf6", # MPEG-1, Layer III (MP3), Protected (CRC used) + b"\xff\xf5", # MPEG-1, Layer III (MP3), No Protection (CRC not used) + b"\xff\xf4", # MPEG-1, Layer III (MP3), Protected (CRC used) + b"\xff\xf3", # MPEG-1, Layer III (MP3), No Protection (CRC not used) + # Layer II (MP2) - Layer Bits = 10 + b"\xff\xfd", # MPEG-1, Layer II (MP2), Protected (CRC used) + b"\xff\xfc", # MPEG-1, Layer II (MP2), Protected (CRC used) + b"\xff\xf9", # MPEG-1, Layer II (MP2), No Protection (CRC not used) + b"\xff\xf8", # MPEG-1, Layer II (MP2), No Protection (CRC not used) + # Layer I (MP1) - Layer Bits = 11 + b"\xff\xff", # MPEG-1, Layer I (MP1), Protected (CRC used) + b"\xff\xfe", # MPEG-1, Layer I (MP1), Protected (CRC used) + b"\xff\xfd", # MPEG-1, Layer I (MP1), No Protection (CRC not used) + b"\xff\xfc", # MPEG-1, Layer I (MP1), No Protection (CRC not used) + b"\xff\xfb", # MPEG-1, Layer I (MP1), Protected (CRC used) + b"\xff\xfa", # MPEG-1, Layer I (MP1), Protected (CRC used) + b"\xff\xf9", # MPEG-1, Layer I (MP1), No Protection (CRC not used) + b"\xff\xf8", # MPEG-1, Layer I (MP1), No Protection (CRC not used) + # MPEG-2 HEADERS (Version Bits = 10) + # Layer III (MP3) - Layer Bits = 01 + b"\xff\xef", # MPEG-2, Layer III (MP3), Protected (CRC used) + b"\xff\xee", # MPEG-2, Layer III (MP3), Protected (CRC used) + b"\xff\xe7", # MPEG-2, Layer III (MP3), No Protection (CRC not used) + b"\xff\xe6", # MPEG-2, Layer III (MP3), No Protection (CRC not used) + b"\xff\xeb", # MPEG-2, Layer III (MP3), Protected (CRC used) + b"\xff\xea", # MPEG-2, Layer III (MP3), Protected (CRC used) + b"\xff\xe5", # MPEG-2, Layer III (MP3), No Protection (CRC not used) + b"\xff\xe4", # MPEG-2, Layer III (MP3), No Protection (CRC not used) + # Layer II (MP2) - Layer Bits = 10 + b"\xff\xed", # MPEG-2, Layer II (MP2), Protected (CRC used) + b"\xff\xec", # MPEG-2, Layer II (MP2), Protected (CRC used) + b"\xff\xe9", # MPEG-2, Layer II (MP2), No Protection (CRC not used) + b"\xff\xe8", # MPEG-2, Layer II (MP2), No Protection (CRC not used) + # Layer I (MP1) - Layer Bits = 11 + b"\xff\xef", # MPEG-2, Layer I (MP1), Protected (CRC used) + b"\xff\xee", # MPEG-2, Layer I (MP1), Protected (CRC used) + b"\xff\xe7", # MPEG-2, Layer I (MP1), No Protection (CRC not used) + b"\xff\xe6", # MPEG-2, Layer I (MP1), No Protection (CRC not used) + b"\xff\xeb", # MPEG-2, Layer I (MP1), Protected (CRC used) + b"\xff\xea", # MPEG-2, Layer I (MP1), Protected (CRC used) + b"\xff\xe5", # MPEG-2, Layer I (MP1), No Protection (CRC not used) + b"\xff\xe4", # MPEG-2, Layer I (MP1), No Protection (CRC not used) + # MPEG-2.5 HEADERS (Version Bits = 00) + # Layer III (MP3) - Layer Bits = 01 + b"\xff\xe3", # MPEG-2.5, Layer III (MP3), Protected (CRC used) + b"\xff\xe2", # MPEG-2.5, Layer III (MP3), Protected (CRC used) + b"\xff\xdb", # MPEG-2.5, Layer III (MP3), No Protection (CRC not used) + b"\xff\xda", # MPEG-2.5, Layer III (MP3), No Protection (CRC not used) + b"\xff\xdf", # MPEG-2.5, Layer III (MP3), Protected (CRC used) + b"\xff\xde", # MPEG-2.5, Layer III (MP3), Protected (CRC used) + b"\xff\xd7", # MPEG-2.5, Layer III (MP3), No Protection (CRC not used) + b"\xff\xd6", # MPEG-2.5, Layer III (MP3), No Protection (CRC not used) +] + + +class DataCache: + """ + We use a data cache as puremagic calls the script more than once. + + Work is performed on first call, cached output is returned in subsequent calls. + This saves doing everything twice. + """ + + _processed_result = None + _file_path = None + _matched = False + + @classmethod + def set_result(cls, result): + """Stores the result after processing.""" + cls._processed_result = result + cls._matched = True + + @classmethod + def set_file_path(cls, file_path: os.PathLike | str): + """Stores the file_path and resets results.""" + cls._file_path = file_path + cls._processed_result = None + cls._matched = False + + @classmethod + def get_result(cls): + """Retrieves the stored result.""" + return cls._processed_result + + @classmethod + def is_matched(cls) -> bool: + """Retrieves the stored result.""" + return cls._matched + + @classmethod + def get_file_path(cls) -> os.PathLike | str: + """Retrieves the file path.""" + return cls._file_path + + @classmethod + def is_cached(cls): + """Checks if the result has been processed yet.""" + return cls._processed_result is not None + + +class EndOfFileTags: + """Processes all end of file tags.""" + + def __init__(self, file_size: int): + self.tags = [] + self.file_size = file_size + self.foot_string = None + self.foot_size = 1572864 # 1.5MB in bytes, changes if file is smaller + + def _id3v1(self) -> bool: + """ + Searches for ID3v1 TAG in last 128 bytes. + + Validation relies on the 'TAG' signature + *AND* either a 4-digit year (1700-3000 seems sensible) + *OR* four null bytes in the Year field + *OR* four spaces (hex 20 used by non compliant encoders/taggers). + + Returns True so we can check for TAG+ or EXT. + Returns None if tag is not valid, no point then checking above. + """ + tag_size = 128 + + if self.foot_size < tag_size: + return False # Too small to contain ID3v1 tag. + + try: + find_tag_loc = self.foot_string.rfind(b"TAG") + if find_tag_loc == -1: + return False # Tag not found + + tag_calc_size = self.foot_size - find_tag_loc + if tag_calc_size != tag_size: + return False # Tag not 128 bytes + + # Year is stored at byte 93 and 97 of TAG + # this should be a 4 digit number, or 4 nulls/spaces + year = self.foot_string[find_tag_loc + 93 : find_tag_loc + 97] + if year == b"\x00\x00\x00\x00" or year == b"\x20\x20\x20\x20": # Check for empty year (all nulls/spaces) + self.tags.append("ID3v1") + return True + try: + year_str = year.decode("ascii", errors="ignore").replace("\x00", "").replace("\x20", "").strip() + if len(year_str) == 4 and year_str.isdigit(): # Check for a plausible 4-digit year 1700-3000 + year_int = int(year_str) + if 1700 <= year_int <= 3000: + self.tags.append("ID3v1") + return True + except ValueError: + pass + + return None # Year could not be found + + except Exception: + return None # Other unexpected issues + + def _tag_plus(self) -> None: + """ + Checks for the ID3v1 Enhanced Tag ('TAG+'). + + This should be located in at 355 bytes from end of file. + There is a chance another tag (like APE or EXT) can push it around, + which means the data could be there, but in the wrong place. + + Validation relies on the 'TAG+' signature, correct tag size, + *AND* either the approved speed bytes (01=slow, 02=medium, 03=fast, 04=hardcore) + *OR* a null byte (00) if unpopulated. + + Returns None as a graceful exit if TAG+ not found + """ + tag_size = 128 + tag_plus_size = 227 + speed_loc = 184 # Speed byte posistion in tag + combined_size = tag_plus_size + tag_size + + if self.foot_size < combined_size: # TAG+ + ID3v1 + return None # Too small to contain TAG+ + + try: + # Scan only calculated tag area, try to avoid false positives + tag_start = self.foot_size - combined_size + tag_end = tag_start + tag_plus_size + find_tag_loc = self.foot_string.rfind(b"TAG+", tag_start, tag_end) + if find_tag_loc == -1: + return None # Tag not found + + tag_calc_size = self.foot_size - find_tag_loc + if tag_calc_size != combined_size: + return None # Tag+ not valid size + + speed_position = find_tag_loc + speed_loc + if 0 <= self.foot_string[speed_position] <= 4: + self.tags.append("TAG+") + else: + return None # Speed byte not in range + + except Exception: + return None # Other unexpected issues + + def _ext_tag(self) -> None: + """ + Checks for the ID3v1.2 Enhanced Tag ('EXT'). + + This should be located at 256 bytes from end of file. + There is a chance another tag (like APE or EXT) can push it around, + which means the data could be there, but in the wrong place. + + Validation relies on the 'EXT' signature and correct tag size. + Unable to validate further as tag has no fixed content. + + Returns None as a graceful exit if EXT not found + """ + tag_size = 128 + ext_tag_size = 128 + combined_size = ext_tag_size + tag_size + + if self.foot_size < combined_size: # EXT + ID3v1 + return None # Too small to contain EXT + + try: + # Scan only calculated tag area, try to avoid false positives + tag_start = self.foot_size - combined_size + tag_end = tag_start + ext_tag_size + find_tag_loc = self.foot_string.rfind(b"EXT", tag_start, tag_end) + if find_tag_loc == -1: + return None # Tag not found + + tag_calc_size = self.foot_size - find_tag_loc + if tag_calc_size != combined_size: + return None # EXT not valid size + else: + self.tags.append("EXT") + + except Exception: + return None # Other unexpected issues + + def _3di(self, id3v1: bool) -> None: + """ + Checks for the rare ID3v1 3DI tag ('3DI'). + + This should be located in either. + a) 10 bytes from end of file if no ID3v1 + b) 10 bytes in front of ID3v1 + There is a chance another tag (like APE or EXT) can push it around, + which means the data could be there, but in the wrong place. + + Validation relies on the '3DI' signature and correct tag size. + Unable to validate further as tag has no fixed content. + + Returns None as a graceful exit if 3DI not found + """ + tag_size = 128 + size_3di = 10 + combined_size = (size_3di + tag_size) if id3v1 else size_3di + if self.foot_size < combined_size: # 3DI OR 3DI + ID3v1 + return None # Too small to contain 3DI + + try: + # Scan only calculated tag area, try to avoid false positives + tag_start = self.foot_size - combined_size + tag_end = tag_start + size_3di + find_tag_loc = self.foot_string.rfind(b"3DI", tag_start, tag_end) + if find_tag_loc == -1: + return None # Tag not found + + tag_calc_size = self.foot_size - find_tag_loc + if tag_calc_size != combined_size: + return None # 3DI not valid size + else: + self.tags.append("3DI") + + except Exception: + return None # Other unexpected issues + + def _lyrics3(self, id3v1: bool) -> None: + """ + Checks for the Lyrics3 v1 and v2. + + These are large tags (upto 1MB) and should be located at either: + a) Upto 1024 bytes from end of file if no ID3v1 + b) Upto 1152 bytes from end of file if ID3v1 present + There is a chance another tag (like APE or EXT) can push it around, + which means the data could be there, but in the wrong place. + + Validation relies on: + a) For v1: LYRICSBEGIN and LYRICSEND + AND a scan for metatag to see if any are present + Unable to validate further as tag has no fixed content. + b) For v2: LYRICSBEGIN and LYRICS200 + AND a scan for metatag to see if any are present + AND check the size of the found tag, matches the size metatag. + + Returns None as a graceful exit if tag block not found + """ + id3v1_size = 128 + max_tag_size = 1048576 # This is on paper the max a Lyrics3 tag could be (v1 in theory has no limit) + combined_size = (max_tag_size + id3v1_size) if id3v1 else max_tag_size + + if self.foot_size < combined_size: # LYRICS OR LYRICS + ID3v1 + combined_size = self.foot_size + + try: + # Scan only calculated tag area, try to avoid false positives + # This just checks for LYRICSEND or LYRICS200 marker immediately + # before EOF or TAG + lyricsend_size = 9 # This is for LYRICSEND or LYRICS200 + end_size = (lyricsend_size + id3v1_size) if id3v1 else lyricsend_size + end_tag_start = self.foot_size - end_size + end_tag_end = end_tag_start + lyricsend_size + found_lyric = None + for lyric_end in (b"LYRICSEND", b"LYRICS200"): + find_tag_end = self.foot_string.rfind(lyric_end, end_tag_start, end_tag_end) + if find_tag_end != -1: + found_lyric = lyric_end + if found_lyric is None: + return None # No end marker found + + # Now we can scan for the start marker, + # as we cannot overly target a scan we go for a broad + # search of the last 1MB (or smaller) of the file. + max_allowed_search_size = min(self.foot_size, self.file_size) + tag_start_start = max_allowed_search_size - combined_size + tag_start_end = tag_start_start + max_tag_size + find_tag_start = self.foot_string.rfind(b"LYRICSBEGIN", tag_start_start, tag_start_end) + if find_tag_start == -1: + return None # Tag start not found + + # Now we have the tag size and can scan inside the tag + lyric3_tags = [b"IND", b"LYR", b"INF", b"AUT", b"EAL", b"EAR", b"ETT", b"IMG", b"GRE"] + tag_block = self.foot_string[find_tag_start:end_tag_end] + + if found_lyric == b"LYRICSEND": # v1 + if any(tag in tag_block for tag in lyric3_tags): # This is the best we can do for v1 + self.tags.append("Lyricsv1") + + if found_lyric == b"LYRICS200": # v2 + # Get the v2 size data + tag_size = len(tag_block) # This matches Hex editors so length is correct + size_tag_size = 6 + v2_tag_data = self.foot_string[end_tag_start - size_tag_size : end_tag_end - lyricsend_size] + v2_tag_size_calc = int(v2_tag_data) + size_tag_size + lyricsend_size + if any(tag in tag_block for tag in lyric3_tags) and tag_size == v2_tag_size_calc: + self.tags.append("Lyricsv2") + + return None # Could not find a valid tag block + + except Exception: + return None # Other unexpected issues + + def _ape(self, id3v1: bool) -> None: + """ + Checks for the Ape v1 and v2. + + These are complicated tags, we currently test for the most common variants. + a) v1 with APETAGEX footer, at end of file or before ID3v1 + b) v2 with APETAGEX header and footer, at end of file or before ID3v1 + There is a chance another tag (like Lyrics3 or EXT) can push it around, + which means the data could be there, but in the wrong place. + + We currently do not test for weird variants such as: + a) v1 lacking the APETAGEX footer + b) v2 lacking the APETAGEX header, footer or both (crazy, but apparently valid) + c) v2 placed at the start of the file + If sample files with these ever appear we can look to test. + + Validation relies on: + a) For v1: finding the APETAGEX footer + AND decode the tag for size and fixed marker checks. + b) For v2: finding the APETAGEX header and footer + AND decode the tag for size and fixed marker checks. + + Returns None as a graceful exit if tag block not found + """ + common_ape_keys = ( + b"Title", + b"Artist", + b"Album", + b"Track", + b"Year", + b"Genre", + b"Comment", + b"Album Artist", + b"Composer", + b"Copyright", + b"Disc", + b"Grouping", + b"Lyrics", + b"Publisher", + b"Subtitle", + b"Performer", + b"Conductor", + b"Rating", + b"File", + b"URL", + b"Cover Art (Front)", # front Titled to match search + b"Cover Art (Back)", # back Titled to match search + b"Media", + b"Language", + b"ReplayGain Track Gain", + b"ReplayGain Track Peak", + b"ReplayGain Album Gain", + b"ReplayGain Album Peak", + b"ISRC", + b"MCN", + ) + id3v1_size = 128 + max_tag_size = 1048576 # This is a pratical scan range of 1MB, Ape v2 in theory can be 4GB + combined_size = (max_tag_size + id3v1_size) if id3v1 else max_tag_size + + if self.foot_size < combined_size: # APE OR APE + ID3v1 + combined_size = self.foot_size + + try: + # Scan only calculated tag area, try to avoid false positives + # This just checks for APETAGEX marker immediately before EOF or ID3v1 TAG + apextag_size = 32 # This is for APETAGEX and data bytes + end_size = (apextag_size + id3v1_size) if id3v1 else apextag_size + end_tag_start = self.foot_size - end_size + end_tag_end = end_tag_start + apextag_size + + find_tag_end = self.foot_string.rfind(b"APETAGEX", end_tag_start, end_tag_end) + if find_tag_end == -1: + return None # Tag not found + + # Footer tag + # Check Version (bytes 8-11, Little-Endian) + f_version = struct.unpack(" None: + """Read last 1.5MB of file and look for tags.""" + file.seek(max(0, self.file_size - self.foot_size)) + self.foot_string = file.read() + self.foot_size = len(self.foot_string) if len(self.foot_string) < self.foot_size else self.foot_size + file.seek(0) + id3v1 = self._id3v1() + if id3v1: # These two require an ID3v1 TAG to be present + self._tag_plus() + self._ext_tag() + self._3di(id3v1) + self._lyrics3(id3v1) + self._ape(id3v1) + + +class MpegAudioDecoder: + """ + Decodes the raw mpeg audio stream. + + This handles Layers I, II and III, with CBR and VBR encodings. + Returns None if any part of the decoding fails. + """ + + def __init__(self): + # --- STATE AND OUTPUT --- + self.tags = [] + self.header_results = {} + self.first_frame_offset = 0 + + # VBR + self.vbr_info = None # Stores detected VBR tag string ("Xing", "VBRI", etc.) + self.VBRI_OFFSET = 36 # Constant for VBRI tag offset + + # --- LOOKUP TABLES --- + self.sample_rate_table = { + 3: [44100, 48000, 32000, 0], # MPEG 1 + 2: [22050, 24000, 16000, 0], # MPEG 2 + 0: [11025, 12000, 8000, 0], # MPEG 2.5 + } + self.bitrate_table = { + # MPEG 1 + 3: { + 3: [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0], # Layer I + 2: [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0], # Layer II + 1: [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0], # Layer III + }, + # MPEG 2/2.5 + 2: { + 3: [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0], # Layer I + 2: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0], # Layer II + 1: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0], # Layer III + }, + 0: { + 3: [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0], # Layer I + 2: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0], # Layer II + 1: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0], # Layer III + }, + } + self.mpeg_version_map = {3: "MPEG 1", 2: "MPEG 2", 0: "MPEG 2.5", 1: "Reserved"} + self.mpeg_version_reverse = {"MPEG 1": 3, "MPEG 2": 2, "MPEG 2.5": 0} + self.layer_map = {3: "Layer I (MP1)", 2: "Layer II (MP2)", 1: "Layer III (MP3)", 0: "Reserved"} + self.channel_mode_map = {3: "Mono", 2: "Dual-Channel", 1: "Joint-Stereo", 0: "Stereo"} + self.vbr_offsets = { + # (MPEG_VERSION_INDEX, IS_MONO) -> Offset from byte 0 + (3, False): 36, # MPEG 1, Stereo/Joint Stereo + (3, True): 21, # MPEG 1, Mono + (2, False): 21, # MPEG 2/2.5, Stereo/Joint Stereo + (2, True): 13, # MPEG 2/2.5, Mono + (0, False): 21, # MPEG 2.5, Stereo + (0, True): 13, # MPEG 2.5, Mono + } + + def _parse_vbr_header(self, frame_bytes: bytes, header_results: Dict[str, Any]) -> Optional[str]: + """ + Checks the first frame for Xing/Info (LAME) and VBRI (Fraunhofer) VBR tags. + + This function relies on self._decode_mp3_header having already passed its + validity checks (sync word, reserved bits, valid rates). + + Returns the tag identifier string ("Xing", "Info", or "VBRI") if a tag is found, + otherwise returns None. + """ + # 1. Basic Validity Checks + if not header_results.get("sync_word") or header_results.get("layer") == "Reserved": + return None + + # VBR headers are for Layer III only + if header_results.get("layer") != "Layer III (MP3)": + return None + + # 2. Determine Offsets using validated results + mpeg_version_str = header_results.get("mpeg_version") + mpeg_version_index = self.mpeg_version_reverse.get(mpeg_version_str) + + if mpeg_version_index is None: + return None + + channel_mode_str = header_results.get("chanel_mode", "Stereo") + is_mono = channel_mode_str == "Mono" + found_tag = None + + # --- 3. Check Xing/Info Tag --- + key = (mpeg_version_index, is_mono) + xing_vbr_offset = self.vbr_offsets.get(key) + + if xing_vbr_offset is not None and len(frame_bytes) >= xing_vbr_offset + 4: + identifier_bytes = frame_bytes[xing_vbr_offset : xing_vbr_offset + 4] + identifier = identifier_bytes.decode("ascii", errors="ignore") + + if identifier in ("Xing", "Info"): + found_tag = identifier + + # --- 4. Check VBRI Tag --- + vbri_vbr_offset = self.VBRI_OFFSET + + # Only check VBRI if Xing/Info was not found + if found_tag is None and len(frame_bytes) >= vbri_vbr_offset + 4: + identifier_bytes = frame_bytes[vbri_vbr_offset : vbri_vbr_offset + 4] + identifier = identifier_bytes.decode("ascii", errors="ignore") + + if identifier == "VBRI": + found_tag = "VBRI" + + # --- 5. Return --- + return found_tag + + def _decode_mp3_header(self, header_bytes: bytes) -> None: + """ + Decodes the 4-byte header. Raises ValueError if invalid. + Performs frame size calculation based on MPEG version and Layer. + """ + header_int = struct.unpack(">I", header_bytes)[0] + + # Extract Fields + sync_word = (header_int >> 20) & 0xFFF + mpeg_version_index = (header_int >> 19) & 0b11 + layer_index = (header_int >> 17) & 0b11 + bit_rate_index = (header_int >> 12) & 0b1111 + sample_rate_index = (header_int >> 10) & 0b11 + padding_bit = (header_int >> 9) & 0b1 + channel_mode_index = (header_int >> 6) & 0b11 + # --- 1. Basic Validation --- + # Sync word can be 0xFFE or 0xFFF + if sync_word < 0xFFE: + raise ValueError("Sync word not fully set.") + if mpeg_version_index == 1 or layer_index == 0: + raise ValueError("Reserved MPEG version or Layer used.") + if bit_rate_index == 0 or bit_rate_index == 15 or sample_rate_index == 3: + raise ValueError("Reserved bit rate, index 0, or sample rate index used.") + # --- 2. Lookup Values --- + sr_list = self.sample_rate_table.get(mpeg_version_index) + if not sr_list: + raise ValueError("MPEG version not supported for Sample Rate lookup.") + sample_rate_hz = sr_list[sample_rate_index] + + br_version_table = self.bitrate_table.get(mpeg_version_index) + if not br_version_table: + raise ValueError("MPEG version not supported for Bit Rate lookup.") + + br_layer_list = br_version_table.get(layer_index) + if not br_layer_list: + raise ValueError("Layer not supported for Bit Rate lookup.") + + bit_rate_kbps = br_layer_list[bit_rate_index] + + if bit_rate_kbps == 0 or sample_rate_hz == 0: + raise ValueError("Calculated bit rate or sample rate is zero.") + + # --- 3. Frame Size Calculation --- + if layer_index == 3: # Layer I + slot_size = 12 + elif layer_index == 2 or layer_index == 1: # Layer II or Layer III + # MPEG 1 uses 144, MPEG 2/2.5 use 72 + if mpeg_version_index == 3: # MPEG 1 + slot_size = 144 + else: # MPEG 2/2.5 + slot_size = 72 + + # Frame Size Formula: + frame_size_val = int((slot_size * bit_rate_kbps * 1000) / sample_rate_hz + padding_bit) + + if layer_index == 3: # Layer I requires multiplication by 4 + frame_size_val *= 4 + + if frame_size_val < 4 or frame_size_val > 5000: + raise ValueError("Calculated frame size is out of expected bounds.") + + # Compile the results dictionary + self.header_results = { + "sync_word": True, + "mpeg_version": self.mpeg_version_map.get(mpeg_version_index), + "layer": self.layer_map.get(layer_index), + "bit_rate": f"{bit_rate_kbps}k", + "sample_rate": f"{sample_rate_hz / 1000:.1f}Khz", + "padding": padding_bit, + "chanel_mode": self.channel_mode_map.get(channel_mode_index, "Reserved"), + "frame_size": f"{frame_size_val} bytes", + "raw_frame_size": frame_size_val, # CRUCIAL for seeking + "bit_rate_index": bit_rate_index, + "mpeg_version_index": mpeg_version_index, + } + + def _check_stream_consistency( + self, file_handle, frame1_bit_rate_index, frame2_start_abs_offset, frame1_size + ) -> str | None: + """ + Checks the bit rate index of the next few frames (up to 3 total) against + the first frame to determine stream consistency, using a small search + window to overcome frame 'wobble' found in some Layer II encodings. + """ + frames_to_check = 2 + current_offset = frame2_start_abs_offset + step_size = frame1_size + + # Loop for Frame 2 and Frame 3 + for i in range(1, frames_to_check + 1): + found_match = False + + # Search window of 4 bytes (0, 1, 2, 3 bytes ahead) + for search_offset in range(4): + seek_pos = current_offset + search_offset + + try: + file_handle.seek(seek_pos, os.SEEK_SET) + frame_header_bytes = file_handle.read(4) + + if len(frame_header_bytes) < 4: + # End of file reached before full consistency check. + # Assume CBR based on checks passed so far. + return "CBR" + + frame_bit_rate_index = self.extract_bit_rate_index(frame_header_bytes) + + # Check 1: Must be a valid header (not -1) AND + # Check 2: Must have the same bit rate index as Frame 1 (frame1_bit_rate_index) + if frame_bit_rate_index != -1 and frame_bit_rate_index == frame1_bit_rate_index: + # Found the next frame at the expected bit rate (CBR). + # Break the inner search loop and prepare for the next frame check. + + # Update the current offset to the *actual* start of the found frame + # plus the expected frame size, for the next check. + current_offset = seek_pos + step_size + found_match = True + break + + except Exception: + continue # Try the next search_offset + + # If we failed to find a consistent frame nearby after checking the window: + if not found_match: + # The bit rate index is inconsistent over a small range. + return "VBR" + + # If the loop completed (F1, F2, and F3 were consistent or found nearby) + return "CBR" + + def extract_bit_rate_index(self, header_bytes): + """Utility to quickly get the bit rate index for stream consistency check.""" + if len(header_bytes) < 4: + return -1 + # Check for sync word (FF Ex) before extracting + if header_bytes[0] != 0xFF or (header_bytes[1] & 0xE0) != 0xE0: + return -1 + + header_int = struct.unpack(">I", header_bytes)[0] + return (header_int >> 12) & 0b1111 + + def decoder(self, head: bytes, file: IO[bytes]): + """Decodes the MPEG Audios Stream.""" + + # Seek to start (start of file or after ID3v2) + file.seek(self.first_frame_offset, os.SEEK_SET) + # Decode the first frame header (H1) + header_bytes_frame1 = file.read(4) + if len(header_bytes_frame1) < 4: + return None + + try: + # Fills self.header_results + self._decode_mp3_header(header_bytes_frame1) + except ValueError: + return None + + raw_frame_size = self.header_results["raw_frame_size"] + + # Read the area for VBR check + read_size_for_vbr_check = min(raw_frame_size - 4, 150) + frame_body_for_vbr = file.read(read_size_for_vbr_check) + + # Combine header and body bytes for easy slicing in the VBR parser + frame_bytes_for_vbr = header_bytes_frame1 + frame_body_for_vbr + + # Check for VBR Header (Xing/Info/VBRI) + # This is only an informative check, we do not determine VBR/CBR from this. + # These headers are for Layer III only, Layers I and II do not have them. + self.vbr_info = self._parse_vbr_header(frame_bytes_for_vbr, self.header_results) + + frame_step_size = raw_frame_size + + # Check Stream Consistency by seeking to Frame 2 and 3 + # This determines VBR/CBR for all MPEG versions and Layers. + if raw_frame_size > 0: + frame2_start_abs_offset = self.first_frame_offset + frame_step_size + frame1_bit_rate_index = self.header_results["bit_rate_index"] + + stream_type_deduction = self._check_stream_consistency( + file, + frame1_bit_rate_index, + frame2_start_abs_offset, + frame_step_size, + ) + else: + stream_type_deduction = None + + # Final Result Compilation + if stream_type_deduction is not None and self.header_results.get("sync_word"): + self.tags = [ + self.header_results["bit_rate"], + self.header_results["sample_rate"], + self.header_results["chanel_mode"], + ] + + self.tags.append(stream_type_deduction) + + return self.tags + + return None + + +class ID3v2Decoder: + """Decodes the ID3v2 tag and calculates the file offset where the audio stream begins.""" + + def __init__(self, file_size: int, mpega: type[Any]): + self.id3v2_tag = None + self.file_size = file_size + self.id3_tag_size = None # Total tag size (10-byte header + content) + + self.tagsv22 = [ # Tag list for ID3v2.2 + b"AEN", + b"BUF", + b"CNT", + b"COM", + b"CRA", + b"CRM", + b"ETC", + b"EQU", + b"GEO", + b"LNK", + b"MCI", + b"MLL", + b"PIC", + b"POP", + b"REV", + b"RVA", + b"SLT", + b"STC", + b"TAL", + b"TBP", + b"TCM", + b"TCO", + b"TCR", + b"TDA", + b"TDY", + b"TEN", + b"TFT", + b"TIM", + b"TKE", + b"TLA", + b"TLE", + b"TMT", + b"TOA", + b"TOF", + b"TOL", + b"TOR", + b"TOT", + b"TP1", + b"TP2", + b"TP3", + b"TP4", + b"TPA", + b"TPB", + b"TRC", + b"TRD", + b"TRK", + b"TSS", + b"TT1", + b"TT2", + b"TT3", + b"TXT", + b"TXX", + b"TYE", + b"UFI", + b"ULT", + b"WAF", + b"WAR", + b"WAS", + b"WCM", + b"WCP", + b"WPB", + b"WXX", + b"WIR", + b"UIN", + ] + # Tag list for ID3v2.3 and 2.4, there are some uniques to both, but not enough + # to make repeating the list beneficial to speed or validity. + self.tagsv23 = [ + b"AENC", + b"APIC", + b"ASPI", + b"COMM", + b"COMR", + b"ENCR", + b"EQU2", + b"ETCO", + b"GEOB", + b"GRID", + b"LINK", + b"MCDI", + b"MLLT", + b"OWNE", + b"PRIV", + b"PCNT", + b"POPM", + b"POSS", + b"RBUF", + b"RVA2", + b"RVRB", + b"SEEK", + b"SIGN", + b"SYLT", + b"SYTC", + b"UFID", + b"USER", + b"USLT", + b"WCOM", + b"WCOP", + b"WOAF", + b"WOAR", + b"WOAS", + b"WORS", + b"WPAY", + b"WPUB", + b"WXXX", + b"TYER", + b"TDAT", + b"TIME", + b"TORY", + b"TALB", + b"TBPM", + b"TCOM", + b"TCON", + b"TCOP", + b"TDEN", + b"TDLY", + b"TDOR", + b"TDRC", + b"TDRL", + b"TDTG", + b"TENC", + b"TEXT", + b"TFLT", + b"TIPL", + b"TIT1", + b"TIT2", + b"TIT3", + b"TKEY", + b"TLAN", + b"TLEN", + b"TMCL", + b"TMED", + b"TMOO", + b"TOAL", + b"TOFN", + b"TOLY", + b"TOPE", + b"TOWN", + b"TPE1", + b"TPE2", + b"TPE3", + b"TPE4", + b"TPOS", + b"TPRO", + b"TPUB", + b"TRCK", + b"TRSN", + b"TRSO", + b"TSOA", + b"TSOC", + b"TSOP", + b"TSOT", + b"TSRC", + b"TSSE", + b"TSST", + b"TXXX", + ] + self.tagsv23_3letter = [ # Super niche 3 letter tags used in ID3v2.3 only + b"WAF", + b"WIR", + b"WYY", + ] + + def _check_id3v2_tag(self, head: bytes) -> Optional[int]: + """ + Checks for ID3v2 tags. Calculates the size of the ID3v2 tag from the + synchsafe size field (bytes 6-9). + + Returns the total tag size (header + content) on success, or None on failure. + """ + + if len(head) < 10: + return None # Header too small + + if head[0:3] != b"ID3": + return None # This should never happen + + size_field = head[6:10] + tag_content_size = 0 + tag = None + + # ID3v2.2 + if head[0:5] == b"ID3\x02\x00": + if head[10:13] not in self.tagsv22: + return None + # ID3v2.2 uses a standard 4-byte big-endian integer for size + tag_content_size = (size_field[0] << 24) | (size_field[1] << 16) | (size_field[2] << 8) | size_field[3] + tag = "ID3v2.2" + + # ID3v2.3 or ID3v2.4 + elif head[0:5] == b"ID3\x03\x00" or head[0:5] == b"ID3\x04\x00": + # Quick tag scan for v2.3/v2.4 (4-letter frames) + if head[10:14] not in self.tagsv23: + # Check for niche 3-letter v2.3 frames + if head[10:13] not in self.tagsv23_3letter: + return None + # ID3v2.3 and ID3v2.4 use the Synchsafe Integer for size + tag_content_size = (size_field[0] << 21) | (size_field[1] << 14) | (size_field[2] << 7) | size_field[3] + tag = "ID3v2.3" if head[0:5] == b"ID3\x03\x00" else "ID3v2.4" + + else: + return None # Invalid tag version + + self.id3v2_tag = tag + self.id3_tag_size = 10 + tag_content_size # Total tag size plus 10-byte header + + # Return the offset where the audio stream starts + return self.id3_tag_size + + def decode_id3v2(self, head: bytes) -> int: + """ + Decodes the ID3v2 tag header (if present at offset 0). + + Returns the absolute file offset where the first audio frame should be + (0 if no ID3v2 tag is found). + """ + audio_start_offset = self._check_id3v2_tag(head) + + # If _check_id3v2_tag was successful, it returns the tag size (the starting offset). + # Otherwise, it returns None, meaning the audio starts at offset 0. + return audio_start_offset if audio_start_offset is not None else 0 + + +def build_name(mpega, id3v2_tags: str, eof_tags: List) -> str | None: + """ + Build an return the full name string and extension. + + Name is constructed from scan results, some examples of final output: + MPEG-1 Audio Layer III (MP3) file [64k 44.1Khz Stereo VBR LAME(Xing) ID3v1 TAG+] + MPEG-1 Audio Layer II (MP2) file [64k 44.1Khz Mono CBR] + MPEG-2 Audio Layer III (MP3) file [64k 24.0Khz Stereo CBR LAME(Info) ID3v2.4] + MPEG-1 Audio Layer I (MP1) file [384k 32.0Khz Stereo CBR] + MPEG-2.5 Audio Layer III (MP3) file [32k 12.0Khz Stereo CBR LAME(Info) ID3v2.4] + MPEG-1 Audio Layer III (MP3) file [160k 44.1Khz Stereo VBR VBRI ID3v2.3] + """ + mpega_results = mpega.header_results + mpega_tags = mpega.tags + vbr_type = mpega.vbr_info + + # Set version: MPEG-1, MPEG-2, MPEG-2.5 + # Reserved if a super rare fringe case that should never happen + version = ( + mpega_results["mpeg_version"].replace(" ", "-") + if mpega_results["mpeg_version"] != "Reserved" + else "MPEG-Unknown Version" + ) + if mpega_results["layer"] == "Layer I (MP1)": + layer = mpega_results["layer"] + ext = ".mp1" + elif mpega_results["layer"] == "Layer II (MP2)": + layer = mpega_results["layer"] + ext = ".mp2" + elif mpega_results["layer"] == "Layer III (MP3)": + layer = mpega_results["layer"] + ext = ".mp3" + else: + # This should never happen + layer = "Unknown Layer" + ext = ".mpga" + name = f"{version} Audio {layer} file" + name_end = "" + name_list = [] + try: + if mpega_tags: + name_list.extend(mpega_tags) # This adds sample, bitrate etc.. + if vbr_type: + tag_name = f"LAME({vbr_type})" if vbr_type in ("Xing", "Info") else vbr_type + name_list.append(tag_name) # Add VBR encoder info for LAME or Fraunhofer + if id3v2_tags: + name_list.append(id3v2_tags) # This adds ID3v2 tag + if eof_tags: + name_list.extend(eof_tags) # This adds tags such as ID3v1, APE, TAG+ etc... + name_end += f" [{' '.join(name_list)}]" + full_name = name + name_end + except Exception: + return None, None # Really should not happen + return full_name, ext + + +def test_mpega(file_path: os.PathLike | str, head: bytes) -> Optional[Match]: + """Main workflow""" + if DataCache.is_cached() and DataCache.get_file_path() == file_path: + if DataCache.is_matched(): + return DataCache.get_result() # Send cached results + else: + return None # No match was made + else: + DataCache.set_file_path(file_path) + eof = EndOfFileTags(os.path.getsize(file_path)) + mpega = MpegAudioDecoder() + id3v2 = ID3v2Decoder(os.path.getsize(file_path), mpega) + try: + with open(file_path, "rb") as file: + eof.find_tags(file) + # If ID3v2 present, test and then adjust frame offset + if b"ID3" == head[0:3]: + mpega.first_frame_offset = id3v2.decode_id3v2(head) + mpega.decoder(head, file) + except Exception: + return None # If the decode process fails for any unknown reason + + full_name, ext = build_name(mpega, id3v2.id3v2_tag, eof.tags) + if full_name is None or ext is None: + return None # Name building failed for some reason + + # Store the result for future calls, then return + result = Match(extension=ext, name=full_name, mime_type="audio/mpeg", confidence=1.0) + DataCache.set_result(result) + return result + + +def main(file_path: os.PathLike | str, head: bytes, _) -> Optional[Match]: + return test_mpega(file_path, head) diff --git a/puremagic/scanners/python_scanner.py b/puremagic/scanners/python_scanner.py index 4ff602d..0293a71 100644 --- a/puremagic/scanners/python_scanner.py +++ b/puremagic/scanners/python_scanner.py @@ -1,10 +1,43 @@ import ast import os +import re from puremagic.scanners.helpers import Match +python_common_keywords = [ + re.compile("\bdef\b"), + re.compile("\bclass\b"), + re.compile("\bimport\b"), + re.compile("\belif\b"), + re.compile("\bwhile\b"), + re.compile("\bexcept\b"), + re.compile("\bfinally\b"), + re.compile("\breturn\b"), + re.compile("\byield\b"), + re.compile("\blambda\b"), + re.compile("\bTrue\b"), + re.compile("\bFalse\b"), + re.compile("\bNone\b"), + re.compile("\b__version__\b"), + re.compile("__main__"), +] -def main(file_path: os.PathLike | str, *_, **__) -> Match | None: +python_patterns = [ + re.compile(r"\bdef\s+\w+\s*\("), # Function definitions + re.compile(r"\bclass\s+\w+\s*[\(:]"), # Class definitions + re.compile(r"\bimport\s+\w+"), # Import statements + re.compile(r"\bfrom\s+\w+\s+import"), # From-import statements + re.compile(r"\bif\s+.*:"), # If statements + re.compile(r"\bfor\s+\w+\s+in\s+.*:"), # For loops + re.compile(r"\bwhile\s+.*:"), # While loops + re.compile(r"\btry\s*:"), # Try blocks + re.compile(r"\.append\("), # Method calls + re.compile(r"\.join\("), # String operations + re.compile(r"print\s*\("), # Print statements +] + + +def main(file_path: os.PathLike | str, _, __) -> Match | None: file_size = os.path.getsize(file_path) if file_size > 1_000_000: return None @@ -12,14 +45,62 @@ def main(file_path: os.PathLike | str, *_, **__) -> Match | None: return None try: - with open(file_path, "r") as file: + with open(file_path, "r", encoding="utf-8") as file: content = file.read() + + # Parse to ensure it's valid Python syntax ast.parse(content) - except Exception: + + if not str(file_path).endswith(".py"): + if not is_substantial_python_code(content): + return None + + except (SyntaxError, UnicodeDecodeError, PermissionError, OSError): return None + return Match( extension=".py", name="Python Script", mime_type="text/x-python", confidence=1.0, ) + + +def is_substantial_python_code(content: str) -> bool: + """ + Check if the content contains substantial Python code indicators. + Returns True if the content appears to be meaningful Python code. + """ + # Remove comments and strings to focus on actual code + content_lines = content.splitlines() + code_lines = [] + + for line in content_lines: + # Remove comments (basic approach - doesn't handle strings containing #) + line = line.split("#")[0].strip() + if line: # Non-empty after removing comments + code_lines.append(line) + + # If too few substantial lines, it's probably not real code + if len(code_lines) < 2: + return False + + code_text = " ".join(code_lines) + + # Check for Python keywords that indicate actual code + + # Count how many keywords are present + keyword_count = 0 + for keyword in python_common_keywords: + if keyword.search(code_text): + keyword_count += 1 + + # Require at least 2 keywords for substantial code + if keyword_count < 2: + return False + + # Check for common Python patterns + for pattern in python_patterns: + if pattern.search(code_text): + return True + return False diff --git a/puremagic/scanners/sndhdr_scanner.py b/puremagic/scanners/sndhdr_scanner.py new file mode 100644 index 0000000..622b65b --- /dev/null +++ b/puremagic/scanners/sndhdr_scanner.py @@ -0,0 +1,46 @@ +""" +Scanner for audio file formats, replacing the functionality of the legacy sndhdr module. + +Other formats are already handled via standard magic_data logic. +""" + +import struct +from typing import Optional + +from puremagic.scanners.helpers import Match + +fssd_match_bytes = b"FSSD" +hcom_match_bytes = b"HCOM" +sndr_match_bytes = b"\0\0" + + +def get_short_le(b: bytes) -> int: + """Get a 2-byte little-endian integer from bytes.""" + return struct.unpack(" Optional[Match]: + """Test for HCOM format.""" + if head[65:69] == b"FSSD" and head[128:132] == b"HCOM": + return Match( + extension=".hcom", + name="Macintosh HCOM Audio File", + mime_type="audio/x-hcom", + confidence=1.0, + ) + return None + + +def main(_, head: bytes, __) -> Optional[Match]: + try: + rate = get_short_le(head[2:4]) + if 4000 <= rate <= 48000: + return Match( + extension=".sndr", + name=f"Macintosh SNDR Resource - {rate} rate", + mime_type="audio/x-sndr", + confidence=0.1, # Lower confidence due to simple format + ) + except (IndexError, struct.error): + pass + return test_hcom(head) diff --git a/puremagic/scanners/text_scanner.py b/puremagic/scanners/text_scanner.py index 75d48ce..0a1a25e 100644 --- a/puremagic/scanners/text_scanner.py +++ b/puremagic/scanners/text_scanner.py @@ -1,32 +1,208 @@ -import os +import csv import re +import os from puremagic.scanners.helpers import Match -crlf_pattern = re.compile(rb"\r\n") -lf_pattern = re.compile(rb"(? tuple[str, str]: + try: + return unicode.decode("ascii"), "ascii" + except UnicodeDecodeError: + pass + for encoding in {"utf-8", "cp1252"}: + try: + return unicode.decode(encoding), encoding + except UnicodeDecodeError: + pass + raise TypeError("No encoding found") + + +def csv_check(file_path, text) -> Match | None: + """ + Validate if content appears to be CSV format. + """ + if not text or len(text.strip()) == 0: + return None + + # Split the text into lines + lines = text.splitlines() + if len(lines) < 2: # Need at least 2 lines to detect a pattern + # If filename ends with .csv, give it the benefit of the doubt + if str(file_path).lower().endswith('.csv'): + return Match( + ".csv", + "Comma-separated values (single line)", + "text/csv", + confidence=0.7 + ) + return None + + # Remove any blank lines + lines = [line for line in lines if line.strip()] + if len(lines) < 2: + return None + if len(lines) > 100: + lines = lines[:-1] # Remove last line in case it's been truncated + + # Try to determine the delimiter by checking common ones + potential_delimiters = [',', ';', '\t', '|', ':'] + delimiter_scores = {} + + for delimiter in potential_delimiters: + # Skip if delimiter isn't in the text + if delimiter not in text: + continue + + # Count fields in each line using this delimiter + field_counts = [len(line.split(delimiter)) for line in lines] + + # Calculate consistency score (higher is better) + if len(field_counts) >= 2: + # Check if most lines have the same number of fields + most_common_count = max(set(field_counts), key=field_counts.count) + matching_lines = sum(1 for count in field_counts if count == most_common_count) + consistency = matching_lines / len(field_counts) + + # More than one field required + if most_common_count > 1: + # Score based on consistency and number of fields + delimiter_scores[delimiter] = (consistency, most_common_count) + + # Try using csv module's Sniffer as a fallback + csv_sniffer_result = None + try: + dialect = csv.Sniffer().sniff(text, delimiters=''.join(potential_delimiters)) + csv_sniffer_result = dialect.delimiter + except Exception: + pass + + # If csv.Sniffer found a delimiter, give it priority + if csv_sniffer_result and csv_sniffer_result in potential_delimiters: + best_delimiter = csv_sniffer_result + confidence = 0.95 + elif delimiter_scores: + # Find best delimiter based on consistency and field count + best_delimiter = max(delimiter_scores.items(), key=lambda x: (x[1][0], x[1][1]))[0] + consistency, field_count = delimiter_scores[best_delimiter] + + # Calculate confidence based on consistency and number of fields + confidence = 0.6 + (consistency * 0.3) + min(0.1, (field_count - 1) * 0.02) + else: + # No clear delimiter pattern found + return None + + # + # # Check for quotes that might indicate CSV + # has_quoted_fields = '"' in text and (f'"{best_delimiter}' in text or f'{best_delimiter}\"') + # + + delimiter_counts = [] + for line in lines: + delim_count = line.count(best_delimiter) + if delim_count == 0: + return None + delimiter_counts.append(delim_count) + + average = sum(delimiter_counts) / len(delimiter_counts) + + max_percentage = 5 / 100 + allowed_deviation = average * max_percentage + + for num in delimiter_counts: + if abs(num - average) > allowed_deviation: + return None + + # Boost confidence if filename ends with .csv + if str(file_path).lower().endswith('.csv'): + confidence = min(1.0, confidence + 0.1) + + # Return match with appropriate confidence + delimiter_name = { + ',': 'comma', + ';': 'semicolon', + '\t': 'tab', + '|': 'pipe', + ':': 'colon' + }.get(best_delimiter, best_delimiter) + + return Match( + ".csv", + f"{delimiter_name}-separated values", + "text/csv", + confidence=confidence + ) + +def file_ending_match(extension, text, mime, file_path): + return Match(extension, text, mime, confidence=1.0 if str(file_path).lower().endswith(extension) else 0.9) + +def dynamic_checks(text, file_path) -> Match | None: + text = text.strip() + if text.startswith("$MeshFormat"): + return file_ending_match(".msh", "Gmsh mesh format", "text/plain", file_path) + if "GenePix ArrayList" in text[:256]: + return file_ending_match(".gal", "Gal GenePix ArrayList", "text/plain", file_path) + if text.startswith("##gff-version"): + return file_ending_match(".gff", "GFF3", "text/plain", file_path) + if "GenePix Results" in text[:256]: + return file_ending_match(".gpr", "GenePix Results", "text/plain", file_path) + if "mzTab-version" in text[:256]: + if "mzTab-version 2" in text[:256]: + return file_ending_match(".mztab2", "mzTab version 2", "text/plain", file_path) + return file_ending_match(".mztab", "mzTab", "text/plain", file_path) + if text.startswith("***tesr"): + return file_ending_match(".tesr", "Neper tesr format", "text/plain", file_path) + if text.startswith("***tess"): + return file_ending_match(".tess", "Neper tess format", "text/plain", file_path) + if text.startswith("# PEFF "): + # consider adding r"# PEFF \d+.\d+" + return file_ending_match(".peff", "PSI Extended FASTA Format", "text/plain", file_path) + if text.startswith("ply") and "format ascii" in text[:128]: + return file_ending_match(".plyascii", "PLY mesh format", "text/plain", file_path) + if text.startswith("RBT_PARAMETER_FILE_V"): + return file_ending_match(".prm", "prm", "text/plain", file_path) + if "# vtk DataFile" in text[:256]: + return file_ending_match(".vtkascii", "vtk", "text/plain", file_path) + if " Match | None: with open(file_path, "rb") as file: head = file.read(1_000_000) - if len(head) < 8: - return Match("", "very short file", "application/octet-stream", confidence=0.5) - try: - head.decode("ascii") - except UnicodeDecodeError: - return Match("", "data", "application/octet-stream", confidence=0.5) - crlf = len(crlf_pattern.findall(head)) - lf = len(lf_pattern.findall(head)) - cr = len(cr_pattern.findall(head)) - if crlf + lf + cr == 0: - return Match(".txt", "ASCII text", "text/plain", confidence=0.9) - - if crlf > lf and crlf > cr: - return Match(".txt", "ASCII text, with CRLF line terminators", "text/plain", confidence=0.9) - if cr > lf and cr > crlf: - return Match(".txt", "ASCII text, with CR line terminators", "text/plain", confidence=0.9) - if lf > cr and lf > crlf: - return Match(".txt", "ASCII text, with LF line terminators", "text/plain", confidence=0.9) - return Match(".txt", "ASCII text", "text/plain", confidence=0.9) + + if len(head) < 8: + return Match("", "very short file", "application/octet-stream", confidence=0.5) + + try: + text, encoding = decode_any(head) + except TypeError: + return Match("", "data", "application/octet-stream", confidence=0.5) + + if csv_match := csv_check(file_path, text): + return csv_match + + if obscure_match := dynamic_checks(text, file_path): + return obscure_match + + crlf = len(crlf_pattern.findall(text)) + lf = len(lf_pattern.findall(text)) + cr = len(cr_pattern.findall(text)) + if crlf + lf + cr == 0: + return Match(".txt", f"{encoding} text", "text/plain", confidence=0.9) + + if crlf > lf and crlf > cr: + return Match(".txt", f"{encoding} text, with CRLF line terminators", "text/plain", confidence=0.9) + if cr > lf and cr > crlf: + return Match(".txt", f"{encoding} text, with CR line terminators", "text/plain", confidence=0.9) + if lf > cr and lf > crlf: + return Match(".txt", f"{encoding} text, with LF line terminators", "text/plain", confidence=0.9) + return Match(".txt", f"{encoding} text", "text/plain", confidence=0.9) diff --git a/pyproject.toml b/pyproject.toml index 39220a0..dd61b96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,14 @@ include = [ "puremagic*" ] [tool.setuptools.dynamic] version = { attr = "puremagic.main.__version__" } +[tool.black] +# Prevent black from linting as we use Ruff now +force-exclude = '.*\.py$' + +[tool.flake8] +# Prevent flake8 from linting as we use Ruff now +exclude = ["*"] + [tool.ruff] target-version = "py312" diff --git a/test/resources/audio/test.flac b/test/resources/audio/test.flac new file mode 100644 index 0000000..fcc9388 Binary files /dev/null and b/test/resources/audio/test.flac differ diff --git a/test/resources/audio/test.opus b/test/resources/audio/test.opus new file mode 100644 index 0000000..37bac1c Binary files /dev/null and b/test/resources/audio/test.opus differ diff --git a/test/resources/audio/test.sndr b/test/resources/audio/test.sndr new file mode 100644 index 0000000..4dd8451 Binary files /dev/null and b/test/resources/audio/test.sndr differ diff --git a/test/resources/audio/test_mp3_vbr_info_128k_notags.mp3 b/test/resources/audio/test_mp3_vbr_info_128k_notags.mp3 new file mode 100644 index 0000000..ba2b7b2 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_info_128k_notags.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3 new file mode 100644 index 0000000..a39d023 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_3di_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3 new file mode 100644 index 0000000..4e23669 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_apev1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3 new file mode 100644 index 0000000..4d94e43 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_apev1_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_apev2_tagplus_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_apev2_tagplus_id3v1.mp3 new file mode 100644 index 0000000..ccc9cfe Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_apev2_tagplus_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3 new file mode 100644 index 0000000..388ad5d Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_ext_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3 new file mode 100644 index 0000000..9f55ac5 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_lyrics3v2_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_notags.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_notags.mp3 new file mode 100644 index 0000000..4e5875d Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_notags.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_apev2_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_apev2_id3v1.mp3 new file mode 100644 index 0000000..89cda10 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_apev2_id3v1.mp3 differ diff --git a/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3 b/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3 new file mode 100644 index 0000000..3e37677 Binary files /dev/null and b/test/resources/audio/test_mp3_vbr_xing_128k_tagplus_id3v1.mp3 differ diff --git a/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3 b/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3 new file mode 100644 index 0000000..b21eb5f Binary files /dev/null and b/test/resources/audio/test_mpeg2_mp3_VBR_128k_id3v2_24.mp3 differ diff --git a/test/resources/system/test_list.json b/test/resources/system/test_list.json new file mode 100644 index 0000000..bace2a0 --- /dev/null +++ b/test/resources/system/test_list.json @@ -0,0 +1 @@ +[1] \ No newline at end of file diff --git a/test/test_common_extensions.py b/test/test_common_extensions.py index a02e5d2..388e9b3 100644 --- a/test/test_common_extensions.py +++ b/test/test_common_extensions.py @@ -217,8 +217,9 @@ def test_cmd_options(): def test_bad_magic_input(): """Test bad magic input""" with pytest.raises(ValueError): - puremagic.main._magic(None, None, None) + puremagic.main.perform_magic(None, None, None) def test_fake_file(): - assert puremagic.magic_file(filename=Path(LOCAL_DIR, "resources", "fake_file"))[0].confidence == 0.5 + results = puremagic.magic_file(filename=Path(LOCAL_DIR, "resources", "fake_file")) + assert results[0].confidence == 0.5, results diff --git a/test/test_main.py b/test/test_main.py deleted file mode 100644 index 649b533..0000000 --- a/test/test_main.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path -from sys import version_info -from warnings import filterwarnings - -import pytest - -from puremagic.main import what - -filterwarnings("ignore", message="'imghdr' is deprecated") -try: # imghdr was removed from the standard library in Python 3.13 - from imghdr import what as imghdr_what -except ModuleNotFoundError: - imghdr_what = None # type: ignore[assignment] - -file_tests = ["bmp", "gif", "jpg", "png", "tif", "webp"] - -here = Path(__file__).resolve().parent - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -@pytest.mark.parametrize("file", file_tests) -def test_what_from_file(file, h=None): - """Run each test with a path string and a pathlib.Path.""" - file = str(here / f"resources/images/test.{file}") - assert what(file, h) == imghdr_what(file, h) - file = Path(file).resolve() - assert what(file, h) == imghdr_what(file, h) - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -def test_what_from_file_none(): - file = str(here / "resources/fake_file") - assert what(file) == imghdr_what(file) is None - file = Path(file).resolve() - assert what(file, None) == imghdr_what(file, None) is None - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -def test_what_from_string_no_str(h="string"): - """what() should raise a TypeError if h is a string.""" - with pytest.raises(TypeError): - imghdr_what(None, h) - with pytest.raises(TypeError) as excinfo: - what(None, h) - assert str(excinfo.value) == "h must be bytes, not str. Consider using bytes.fromhex(h)" - - -string_tests = [ - ("bmp", "424d"), - ("bmp", "424d787878785c3030305c303030"), - ("bmp", b"BM"), - ("exr", "762f3101"), - ("exr", b"\x76\x2f\x31\x01"), - ("exr", b"v/1\x01"), - ("gif", "474946383761"), - ("gif", "474946383961"), - ("gif", b"GIF87a"), - ("gif", b"GIF89a"), - ("pbm", b"P1 "), - ("pbm", b"P1\n"), - ("pbm", b"P1\r"), - ("pbm", b"P1\t"), - ("pbm", b"P4 "), - ("pbm", b"P4\n"), - ("pbm", b"P4\r"), - ("pbm", b"P4\t"), - ("pgm", b"P2 "), - ("pgm", b"P2\n"), - ("pgm", b"P2\r"), - ("pgm", b"P2\t"), - ("pgm", b"P5 "), - ("pgm", b"P5\n"), - ("pgm", b"P5\r"), - ("pgm", b"P5\t"), - ("png", "89504e470d0a1a0a"), - ("png", b"\211PNG\r\n\032\n"), - ("png", b"\x89PNG\r\n\x1a\n"), - ("ppm", b"P3 "), - ("ppm", b"P3\n"), - ("ppm", b"P3\r"), - ("ppm", b"P3\t"), - ("ppm", b"P6 "), - ("ppm", b"P6\n"), - ("ppm", b"P6\r"), - ("ppm", b"P6\t"), - ("rast", "59A66A95"), - ("rast", b"\x59\xa6\x6a\x95"), - ("rgb", "01da"), - ("rgb", b"\x01\xda"), - ("tiff", "49492a00"), - ("tiff", "4d4d002a"), - ("tiff", "4d4d002b"), - ("tiff", b"II*\x00"), # bytes.fromhex('49492a00') - ("tiff", b"MM\x00*"), # bytes.fromhex('4d4d002a') - ("tiff", b"MM\x00+"), # bytes.fromhex('4d4d002b') - ("webp", b"RIFF____WEBP"), - ("xbm", b"#define "), - (None, "decafbad"), - (None, b"decafbad"), -] - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -@pytest.mark.parametrize("expected, h", string_tests) -def test_what_from_string(expected, h): - if isinstance(h, str): # In imgdir.what() h must be bytes, not str. - h = bytes.fromhex(h) # ex. "474946383761" --> b"GIF87a" - assert imghdr_what(None, h) == what(None, h) == expected - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -@pytest.mark.parametrize( - "expected, h", - [ - ("jpeg", "ffd8ffdb"), - ("jpeg", b"\xff\xd8\xff\xdb"), - ], -) -def test_what_from_string_py311(expected, h): - """ - These tests fail with imghdr on Python < 3.11. - """ - if isinstance(h, str): # In imgdir.what() h must be bytes, not str. - h = bytes.fromhex(h) - assert what(None, h) == expected - if version_info < (3, 11): # TODO: Document these imghdr fails - expected = None - assert imghdr_what(None, h) == expected - - -@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") -@pytest.mark.parametrize( - "expected, h", - [ - ("jpeg", b"______Exif"), - ("jpeg", b"______Exif"), - ("jpeg", b"______JFIF"), - ("jpeg", b"______JFIF"), - ("tiff", "4949"), - ("tiff", "49495c7832615c783030"), - ("tiff", "4d4d"), - ("tiff", "4d4d5c7830305c783261"), - ("tiff", b"II"), # bytes.fromhex('4949') - ("tiff", b"II\\x2a\\x00"), # bytes.fromhex('49495c7832615c783030') - ("tiff", b"MM"), # bytes.fromhex('4d4d') - ("tiff", b"MM\\x00\\x2a"), # bytes.fromhex('4d4d5c7830305c783261') - ], -) -@pytest.mark.parametrize("imghdr_strict", [True, False]) -def test_what_from_string_imghdr_strict(expected, h, imghdr_strict): - """ - These tests pass with imghdr but fail with puremagic. - """ - if isinstance(h, str): # In imgdir.what() h must be bytes, not str. - h = bytes.fromhex(h) - assert imghdr_what(None, h) == expected - assert what(None, h, imghdr_strict) == (expected if imghdr_strict else None) diff --git a/test/test_scanners.py b/test/test_scanners.py index fb38423..5352488 100644 --- a/test/test_scanners.py +++ b/test/test_scanners.py @@ -1,6 +1,6 @@ import puremagic -from test.common import OFFICE_DIR, SYSTEM_DIR -from puremagic.scanners import python_scanner, json_scanner +from test.common import OFFICE_DIR, SYSTEM_DIR, AUDIO_DIR +from puremagic.scanners import python_scanner, json_scanner, sndhdr_scanner sample_text = b"""Lorem ipsum dolor sit amet, consectetur adipiscing elit,{ending} sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.{ending} @@ -17,7 +17,7 @@ def test_text_scanner(): lr_file.write_bytes(sample_text.replace(b"\n", b"").replace(b"{ending}", b"\n")) results = puremagic.magic_file(lr_file) assert results[0].extension == ".txt" - assert results[0].name == "ASCII text, with LF line terminators" + assert results[0].name == "ascii text, with LF line terminators" assert results[0].mime_type == "text/plain" assert results[0].confidence == 0.9 @@ -25,15 +25,15 @@ def test_text_scanner(): crlf_file.write_bytes(sample_text.replace(b"\n", b"").replace(b"{ending}", b"\r\n")) results = puremagic.magic_file(crlf_file) assert results[0].extension == ".txt" - assert results[0].name == "ASCII text, with CRLF line terminators" + assert results[0].name == "ascii text, with CRLF line terminators" assert results[0].mime_type == "text/plain" assert results[0].confidence == 0.9 cr_file = OFFICE_DIR / "text_cr.txt" cr_file.write_bytes(sample_text.replace(b"\n", b"").replace(b"{ending}", b"\r")) results = puremagic.magic_file(cr_file) + assert results[0].name == "ascii text, with CR line terminators" assert results[0].extension == ".txt" - assert results[0].name == "ASCII text, with CR line terminators" assert results[0].mime_type == "text/plain" assert results[0].confidence == 0.9 @@ -41,10 +41,10 @@ def test_text_scanner(): def test_python_scanner(): # Test the Python scanner with a sample Python file py_file = SYSTEM_DIR / "test.py" - result = python_scanner.main(py_file) + result = python_scanner.main(py_file, None, None) magic_result = puremagic.magic_file(py_file) - assert result.confidence == magic_result[0].confidence assert result.extension == ".py" + assert result.confidence == magic_result[0].confidence assert result.name == "Python Script" assert result.mime_type == "text/x-python" assert result.confidence == 1.0 @@ -59,3 +59,17 @@ def test_json_scanner(): assert result.name == "JSON File" assert result.mime_type == "application/json" assert result.confidence == 1.0 + + +def test_sndhdr_scanner(): + # Test the sndhdr scanner with sndr file + sndr_file = AUDIO_DIR / "test.sndr" + with open(sndr_file, "rb") as f: + head = f.read(512) + result = sndhdr_scanner.main(None, head, None) + puremagic.magic_file(sndr_file) + assert result is not None + assert result.extension == ".sndr" + assert result.name.startswith("Macintosh SNDR Resource") + assert result.mime_type == "audio/x-sndr" + assert result.confidence == 0.1