From ff3e0498e87a4e4f19c90d8e20456aee78dc37a8 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Tue, 3 Mar 2026 10:05:56 +0500 Subject: [PATCH 1/8] Drop unused imports --- magic/__init__.py | 6 +----- magic/compat.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/magic/__init__.py b/magic/__init__.py index 851b717f..14d18968 100644 --- a/magic/__init__.py +++ b/magic/__init__.py @@ -18,11 +18,7 @@ import sys import os -import glob -import ctypes -import ctypes.util import threading -import logging from ctypes import c_char_p, c_int, c_size_t, c_void_p, byref, POINTER @@ -252,7 +248,7 @@ def from_descriptor(fd, mime=False): libmagic = loader.load_lib() -magic_t = ctypes.c_void_p +magic_t = c_void_p def errorcheck_null(result, func, args): diff --git a/magic/compat.py b/magic/compat.py index 6ab9400e..32a7b93b 100644 --- a/magic/compat.py +++ b/magic/compat.py @@ -8,7 +8,6 @@ from collections import namedtuple from ctypes import * -from ctypes.util import find_library from . import loader From a1fad4334ca36f3263dc9e30d6dbb01dec1eed71 Mon Sep 17 00:00:00 2001 From: Adam Thompson-Sharpe Date: Wed, 6 May 2026 12:15:43 -0400 Subject: [PATCH 2/8] Fix test for Apache Parquet files for file 5.47 The output for Parquet files changed in 5.47. This commit edits the test to accept both the old and new output. ```sh # Old $ file example.parquet example.parquet: Apache Parquet $ file --mime example.parquet example.parquet: application/octet-stream; charset=binary # New $ file example.parquet example.parquet: Apache Parquet file $ file --mime example.parquet example.parquet: application/vnd.apache.parquet; charset=binary ``` --- test/python_magic_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python_magic_test.py b/test/python_magic_test.py index 50760440..3719b9f6 100755 --- a/test/python_magic_test.py +++ b/test/python_magic_test.py @@ -89,8 +89,8 @@ class TestFile: (NO_SOFT, ["data"]), ], b"test.snappy.parquet": [ - (COMMON_MIME, ["application/octet-stream"]), - (COMMON_PLAIN, ["Apache Parquet", "Par archive data"]), + (COMMON_MIME, ["application/octet-stream", "application/vnd.apache.parquet"]), + (COMMON_PLAIN, ["Apache Parquet", "Apache Parquet file", "Par archive data"]), (NO_SOFT, ["data"]), ], b"test.json": [ From 71301b0d4f84734116b8d834cb873548dbf09b51 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:09:18 +0300 Subject: [PATCH 3/8] Add python 3.14 to CI --- .github/workflows/ci.yml | 2 +- tox.ini | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab0c6435..83d84d9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"] include: - os: macos-latest python-version: "3.x" diff --git a/tox.ini b/tox.ini index 5c1648b3..51b12cfd 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,9 @@ envlist = py311, py312, py313, + py313t, + py314, + py314t, mypy [testenv] From 8e7d98e11632f33f714b057ff1052cb13bd69ac2 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:23:53 +0300 Subject: [PATCH 4/8] Move lock to global scope --- magic/__init__.py | 67 +++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/magic/__init__.py b/magic/__init__.py index 14d18968..fbdc3889 100644 --- a/magic/__init__.py +++ b/magic/__init__.py @@ -105,7 +105,6 @@ def __init__( self.flags |= MAGIC_NO_CHECK_SIMH self.cookie = magic_open(self.flags) - self.lock = threading.Lock() magic_load(self.cookie, magic_file) @@ -134,34 +133,31 @@ def from_buffer(self, buf): """ Identify the contents of `buf` """ - with self.lock: - try: - # if we're on python3, convert buf to bytes - # otherwise this string is passed as wchar* - # which is not what libmagic expects - # NEXTBREAK: only take bytes - if type(buf) == str and str != bytes: - buf = buf.encode("utf-8", errors="replace") - return maybe_decode(magic_buffer(self.cookie, buf)) - except MagicException as e: - return self._handle509Bug(e) + try: + # if we're on python3, convert buf to bytes + # otherwise this string is passed as wchar* + # which is not what libmagic expects + # NEXTBREAK: only take bytes + if type(buf) == str and str != bytes: + buf = buf.encode("utf-8", errors="replace") + return maybe_decode(magic_buffer(self.cookie, buf)) + except MagicException as e: + return self._handle509Bug(e) def from_file(self, filename): # raise FileNotFoundException or IOError if the file does not exist os.stat(filename, follow_symlinks=self.flags & MAGIC_SYMLINK) - with self.lock: - try: - return maybe_decode(magic_file(self.cookie, filename)) - except MagicException as e: - return self._handle509Bug(e) + try: + return maybe_decode(magic_file(self.cookie, filename)) + except MagicException as e: + return self._handle509Bug(e) def from_descriptor(self, fd): - with self.lock: - try: - return maybe_decode(magic_descriptor(self.cookie, fd)) - except MagicException as e: - return self._handle509Bug(e) + try: + return maybe_decode(magic_descriptor(self.cookie, fd)) + except MagicException as e: + return self._handle509Bug(e) def _handle509Bug(self, e): # libmagic 5.09 has a bug where it might fail to identify the @@ -313,6 +309,9 @@ def coerce_filename(filename): return filename +# libmagic is not thread-safe: guard for concurrent calls on a global scope +LOCK = threading.Lock() + magic_open = libmagic.magic_open magic_open.restype = magic_t magic_open.argtypes = [c_int] @@ -336,7 +335,8 @@ def coerce_filename(filename): def magic_file(cookie, filename): - return _magic_file(cookie, coerce_filename(filename)) + with LOCK: + return _magic_file(cookie, coerce_filename(filename)) _magic_buffer = libmagic.magic_buffer @@ -346,7 +346,8 @@ def magic_file(cookie, filename): def magic_buffer(cookie, buf): - return _magic_buffer(cookie, buf, len(buf)) + with LOCK: + return _magic_buffer(cookie, buf, len(buf)) magic_descriptor = libmagic.magic_descriptor @@ -361,7 +362,8 @@ def magic_buffer(cookie, buf): def magic_descriptor(cookie, fd): - return _magic_descriptor(cookie, fd) + with LOCK: + return _magic_descriptor(cookie, fd) _magic_load = libmagic.magic_load @@ -371,7 +373,8 @@ def magic_descriptor(cookie, fd): def magic_load(cookie, filename): - return _magic_load(cookie, coerce_filename(filename)) + with LOCK: + return _magic_load(cookie, coerce_filename(filename)) magic_setflags = libmagic.magic_setflags @@ -404,15 +407,16 @@ def magic_setparam(cookie, param, val): if not _has_param: raise NotImplementedError("magic_setparam not implemented") v = c_size_t(val) - return _magic_setparam(cookie, param, byref(v)) + with LOCK: + return _magic_setparam(cookie, param, byref(v)) def magic_getparam(cookie, param): if not _has_param: raise NotImplementedError("magic_getparam not implemented") val = c_size_t() - _magic_getparam(cookie, param, byref(val)) - return val.value + with LOCK: + return _magic_getparam(cookie, param, byref(val)).value _has_version = False @@ -423,10 +427,11 @@ def magic_getparam(cookie, param): magic_version.argtypes = [] -def version(): +def version(lock=None): if not _has_version: raise NotImplementedError("magic_version not implemented") - return magic_version() + with LOCK: + return magic_version() MAGIC_NONE = 0x000000 # No flags From 892543d4c575c31eea1dd68220e287a67ee98bee Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:39:45 +0300 Subject: [PATCH 5/8] Add test --- magic/__init__.py | 5 +++-- test/python_magic_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/magic/__init__.py b/magic/__init__.py index fbdc3889..21af9c45 100644 --- a/magic/__init__.py +++ b/magic/__init__.py @@ -416,7 +416,8 @@ def magic_getparam(cookie, param): raise NotImplementedError("magic_getparam not implemented") val = c_size_t() with LOCK: - return _magic_getparam(cookie, param, byref(val)).value + _magic_getparam(cookie, param, byref(val)) + return val.value _has_version = False @@ -427,7 +428,7 @@ def magic_getparam(cookie, param): magic_version.argtypes = [] -def version(lock=None): +def version(): if not _has_version: raise NotImplementedError("magic_version not implemented") with LOCK: diff --git a/test/python_magic_test.py b/test/python_magic_test.py index 3719b9f6..26398614 100755 --- a/test/python_magic_test.py +++ b/test/python_magic_test.py @@ -10,6 +10,12 @@ import pytest +try: + from concurrent.futures import ThreadPoolExecutor + HAS_CONCURRENT_FUTURES = True +except ImportError: # python 2.7 + HAS_CONCURRENT_FUTURES = False + # for output which reports a local time os.environ["TZ"] = "GMT" @@ -321,6 +327,25 @@ def test_symlink(self): self.assertRaises(IOError, m_follow.from_file, tmp_broken) + @unittest.skipIf(not HAS_CONCURRENT_FUTURES, "concurrent.futures not available in Python 2.7") + def test_thread_safety(self): + """Test that concurrent from_file calls don't crash (would SEGV without global lock)""" + filename = os.path.join(self.TESTDATA_DIR, "test.pdf") + + m = magic.Magic(mime=True) + + def check_file(_): + result = m.from_file(filename) + self.assertEqual(result, "application/pdf") + return result + + with ThreadPoolExecutor(100) as executor: + results = list(executor.map(check_file, range(100))) + + # All calls should complete successfully + self.assertEqual(len(results), 100) + self.assertTrue(all(r == "application/pdf" for r in results)) + if __name__ == "__main__": unittest.main() From f3cef270ce31c64d451dfd6cd0784a610f78addb Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:44:09 +0200 Subject: [PATCH 6/8] Apply suggestions from code review --- .github/workflows/ci.yml | 2 +- tox.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d84d9c..ddcbd25c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] include: - os: macos-latest python-version: "3.x" diff --git a/tox.ini b/tox.ini index 51b12cfd..01cb7b23 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ envlist = py311, py312, py313, - py313t, py314, py314t, mypy From 4043553f3d4116bcf27fc4acb64da55b25dc7f65 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:04:26 +0300 Subject: [PATCH 7/8] Revert "Move lock to global scope" This reverts commit f2ac98d8aa7464165984068de9e484d0321cd4f3. --- magic/__init__.py | 64 +++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/magic/__init__.py b/magic/__init__.py index 21af9c45..14d18968 100644 --- a/magic/__init__.py +++ b/magic/__init__.py @@ -105,6 +105,7 @@ def __init__( self.flags |= MAGIC_NO_CHECK_SIMH self.cookie = magic_open(self.flags) + self.lock = threading.Lock() magic_load(self.cookie, magic_file) @@ -133,31 +134,34 @@ def from_buffer(self, buf): """ Identify the contents of `buf` """ - try: - # if we're on python3, convert buf to bytes - # otherwise this string is passed as wchar* - # which is not what libmagic expects - # NEXTBREAK: only take bytes - if type(buf) == str and str != bytes: - buf = buf.encode("utf-8", errors="replace") - return maybe_decode(magic_buffer(self.cookie, buf)) - except MagicException as e: - return self._handle509Bug(e) + with self.lock: + try: + # if we're on python3, convert buf to bytes + # otherwise this string is passed as wchar* + # which is not what libmagic expects + # NEXTBREAK: only take bytes + if type(buf) == str and str != bytes: + buf = buf.encode("utf-8", errors="replace") + return maybe_decode(magic_buffer(self.cookie, buf)) + except MagicException as e: + return self._handle509Bug(e) def from_file(self, filename): # raise FileNotFoundException or IOError if the file does not exist os.stat(filename, follow_symlinks=self.flags & MAGIC_SYMLINK) - try: - return maybe_decode(magic_file(self.cookie, filename)) - except MagicException as e: - return self._handle509Bug(e) + with self.lock: + try: + return maybe_decode(magic_file(self.cookie, filename)) + except MagicException as e: + return self._handle509Bug(e) def from_descriptor(self, fd): - try: - return maybe_decode(magic_descriptor(self.cookie, fd)) - except MagicException as e: - return self._handle509Bug(e) + with self.lock: + try: + return maybe_decode(magic_descriptor(self.cookie, fd)) + except MagicException as e: + return self._handle509Bug(e) def _handle509Bug(self, e): # libmagic 5.09 has a bug where it might fail to identify the @@ -309,9 +313,6 @@ def coerce_filename(filename): return filename -# libmagic is not thread-safe: guard for concurrent calls on a global scope -LOCK = threading.Lock() - magic_open = libmagic.magic_open magic_open.restype = magic_t magic_open.argtypes = [c_int] @@ -335,8 +336,7 @@ def coerce_filename(filename): def magic_file(cookie, filename): - with LOCK: - return _magic_file(cookie, coerce_filename(filename)) + return _magic_file(cookie, coerce_filename(filename)) _magic_buffer = libmagic.magic_buffer @@ -346,8 +346,7 @@ def magic_file(cookie, filename): def magic_buffer(cookie, buf): - with LOCK: - return _magic_buffer(cookie, buf, len(buf)) + return _magic_buffer(cookie, buf, len(buf)) magic_descriptor = libmagic.magic_descriptor @@ -362,8 +361,7 @@ def magic_buffer(cookie, buf): def magic_descriptor(cookie, fd): - with LOCK: - return _magic_descriptor(cookie, fd) + return _magic_descriptor(cookie, fd) _magic_load = libmagic.magic_load @@ -373,8 +371,7 @@ def magic_descriptor(cookie, fd): def magic_load(cookie, filename): - with LOCK: - return _magic_load(cookie, coerce_filename(filename)) + return _magic_load(cookie, coerce_filename(filename)) magic_setflags = libmagic.magic_setflags @@ -407,16 +404,14 @@ def magic_setparam(cookie, param, val): if not _has_param: raise NotImplementedError("magic_setparam not implemented") v = c_size_t(val) - with LOCK: - return _magic_setparam(cookie, param, byref(v)) + return _magic_setparam(cookie, param, byref(v)) def magic_getparam(cookie, param): if not _has_param: raise NotImplementedError("magic_getparam not implemented") val = c_size_t() - with LOCK: - _magic_getparam(cookie, param, byref(val)) + _magic_getparam(cookie, param, byref(val)) return val.value @@ -431,8 +426,7 @@ def magic_getparam(cookie, param): def version(): if not _has_version: raise NotImplementedError("magic_version not implemented") - with LOCK: - return magic_version() + return magic_version() MAGIC_NONE = 0x000000 # No flags From 9e0d5a780208567f06bbe47b042c43ac359c57bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:24:23 +0000 Subject: [PATCH 8/8] Bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) Updates `actions/setup-python` from 6 to 7 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddcbd25c..772132e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: # python-version: '3.13' runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true