0.4.27-19-g2a01b18.add-magic-symlink-support-and-tests-for-same.patch 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. Subject: Add MAGIC_SYMLINK support, and tests for same
  2. Origin: upstream, commit 0.4.27-19-g2a01b18 <https://github.com/ahupp/python-magic/commit/0.4.27-19-g2a01b18>
  3. Author: Adam Hupp <adam@hupp.org>
  4. Date: Fri Aug 25 11:02:53 2023 -0700
  5. --- a/CHANGELOG
  6. +++ b/CHANGELOG
  7. @@ -1,41 +1,51 @@
  8. +Changes to 0.4.29:
  9. +
  10. +- support MAGIC_SYMLINK (via follow_symlink flag on Magic constructor)
  11. +- correctly throw FileNotFoundException depending on flag
  12. +
  13. Changes to 0.4.28:
  14. - - support "magic-1.dll" on Windows, which is produced by vcpkg
  15. - - add python 3.10 to tox config
  16. - - update test for upstream gzip extensions
  17. +
  18. +- support "magic-1.dll" on Windows, which is produced by vcpkg
  19. +- add python 3.10 to tox config
  20. +- update test for upstream gzip extensions
  21. Changes to 0.4.27:
  22. - - remove spurious pyproject.toml that breaks source builds
  23. +
  24. +- remove spurious pyproject.toml that breaks source builds
  25. Changes to 0.4.26:
  26. - - Use tox for all multi-version testing
  27. - - Fix use of pytest, use it via tox
  28. +
  29. +- Use tox for all multi-version testing
  30. +- Fix use of pytest, use it via tox
  31. Changes to 0.4.25:
  32. - - Support os.PathLike values in Magic.from_file and magic.from_file
  33. - - Handle some versions of libmagic that return mime string without charset
  34. - - Fix tests for file 5.41
  35. - - Include typing stub in package
  36. +
  37. +- Support os.PathLike values in Magic.from_file and magic.from_file
  38. +- Handle some versions of libmagic that return mime string without charset
  39. +- Fix tests for file 5.41
  40. +- Include typing stub in package
  41. Changes to 0.4.24:
  42. - - Fix regression in library loading on some Alpine docker images.
  43. +
  44. +- Fix regression in library loading on some Alpine docker images.
  45. Changes to 0.4.23
  46. - - Include a `py.typed` sentinal to enable type checking
  47. - - Improve fix for attribute error during destruction
  48. - - Cleanup library loading logic
  49. - - Add new homebrew library dir for OSX
  50. +- Include a `py.typed` sentinal to enable type checking
  51. +- Improve fix for attribute error during destruction
  52. +- Cleanup library loading logic
  53. +- Add new homebrew library dir for OSX
  54. Changes to 0.4.21, 0.4.22
  55. - - Unify dll loader between the standard and compat library, fixing load
  56. - failures on some previously supported platforms.
  57. +- Unify dll loader between the standard and compat library, fixing load
  58. + failures on some previously supported platforms.
  59. Changes to 0.4.20
  60. - merge in a compatibility layer for the upstream libmagic python binding.
  61. Since both this package and that one are called 'magic', this compat layer
  62. - removes a very common source of runtime errors. Use of that libmagic API will
  63. + removes a very common source of runtime errors. Use of that libmagic API will
  64. produce a deprecation warning.
  65. - support python 3.9 in tests and pypi metadata
  66. @@ -44,9 +54,9 @@
  67. rather than a filename.
  68. - sometimes the returned description includes snippets of the file, e.g a title
  69. - for MS Word docs. Since this is in an unknown encoding, we would throw a
  70. - unicode decode error trying to decode. Now, it decodes with
  71. - 'backslashreplace' to handle this more gracefully. The undecodable characters
  72. + for MS Word docs. Since this is in an unknown encoding, we would throw a
  73. + unicode decode error trying to decode. Now, it decodes with
  74. + 'backslashreplace' to handle this more gracefully. The undecodable characters
  75. are replaced with hex escapes.
  76. - add support for MAGIC_EXTENSION, to return possible file extensions.
  77. @@ -55,18 +65,18 @@
  78. Changes in 0.4.18
  79. -- Make bindings for magic_[set|get]param optional, and throw NotImplementedError
  80. -if they are used but not supported. Only call setparam() in the constructor if
  81. -it's supported. This prevents breakage on CentOS7 which uses an old version of
  82. -libmagic.
  83. +- Make bindings for magic\_[set|get]param optional, and throw NotImplementedError
  84. + if they are used but not supported. Only call setparam() in the constructor if
  85. + it's supported. This prevents breakage on CentOS7 which uses an old version of
  86. + libmagic.
  87. - Add tests for CentOS 7 & 8
  88. Changes in 0.4.16 and 0.4.17
  89. - add MAGIC_MIME_TYPE constant, use that in preference to MAGIC_MIME internally.
  90. -This sets up for a breaking change in a future major version bump where
  91. -MAGIC_MIME will change to mathch magic.h.
  92. + This sets up for a breaking change in a future major version bump where
  93. + MAGIC_MIME will change to mathch magic.h.
  94. - add magic.version() function to return library version
  95. - add setparam/getparam to control internal behavior
  96. - increase internal limits with setparam to prevent spurious error on some jpeg files
  97. @@ -76,12 +86,12 @@
  98. - include tests in source distribution
  99. - many test improvements:
  100. --- tox runner support
  101. --- remove deprecated test_suite field from setup.py
  102. --- docker tests that cover all LTS ubuntu versions
  103. --- add test for snapp file identification
  104. + -- tox runner support
  105. + -- remove deprecated test_suite field from setup.py
  106. + -- docker tests that cover all LTS ubuntu versions
  107. + -- add test for snapp file identification
  108. - doc improvements
  109. --- document dependency install process for debian
  110. --- various typos
  111. --- document test running process
  112. + -- document dependency install process for debian
  113. + -- various typos
  114. + -- document test running process
  115. --- a/magic/__init__.py
  116. +++ b/magic/__init__.py
  117. @@ -39,7 +39,8 @@
  118. """
  119. def __init__(self, mime=False, magic_file=None, mime_encoding=False,
  120. - keep_going=False, uncompress=False, raw=False, extension=False):
  121. + keep_going=False, uncompress=False, raw=False, extension=False,
  122. + follow_symlinks=False):
  123. """
  124. Create a new libmagic wrapper.
  125. @@ -65,6 +66,9 @@
  126. if extension:
  127. self.flags |= MAGIC_EXTENSION
  128. + if follow_symlinks:
  129. + self.flags |= MAGIC_SYMLINK
  130. +
  131. self.cookie = magic_open(self.flags)
  132. self.lock = threading.Lock()
  133. --- a/test/README
  134. +++ b/test/README
  135. @@ -1,6 +1,4 @@
  136. There are a few ways to run the python-magic tests
  137. - 1. `pytest` will run the test suite against your default version of python
  138. - 2. `./test/run_all_versions.py` will run the tests against all installed versions of python.
  139. - 3. `./test/run_all_docker_test.sh` will run against a variety of different Linux distributions, using docker.
  140. -
  141. +1. `tox` will run the tests against all installed versions of python
  142. +2. `./test/run_all_docker_test.sh` will run against a variety of different Linux distributions, using docker.
  143. --- a/test/python_magic_test.py
  144. +++ b/test/python_magic_test.py
  145. @@ -1,9 +1,10 @@
  146. +import tempfile
  147. import os
  148. # for output which reports a local time
  149. -os.environ['TZ'] = 'GMT'
  150. +os.environ["TZ"] = "GMT"
  151. -if os.environ.get('LC_ALL', '') != 'en_US.UTF-8':
  152. +if os.environ.get("LC_ALL", "") != "en_US.UTF-8":
  153. # this ensure we're in a utf-8 default filesystem encoding which is
  154. # necessary for some tests
  155. raise Exception("must run `export LC_ALL=en_US.UTF-8` before running test suite")
  156. @@ -16,10 +17,11 @@
  157. import sys
  158. # magic_descriptor is broken (?) in centos 7, so don't run those tests
  159. -SKIP_FROM_DESCRIPTOR = bool(os.environ.get('SKIP_FROM_DESCRIPTOR'))
  160. +SKIP_FROM_DESCRIPTOR = bool(os.environ.get("SKIP_FROM_DESCRIPTOR"))
  161. +
  162. class MagicTest(unittest.TestCase):
  163. - TESTDATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'testdata'))
  164. + TESTDATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "testdata"))
  165. def test_version(self):
  166. try:
  167. @@ -28,20 +30,19 @@
  168. pass
  169. def test_fs_encoding(self):
  170. - self.assertEqual('utf-8', sys.getfilesystemencoding().lower())
  171. + self.assertEqual("utf-8", sys.getfilesystemencoding().lower())
  172. def assert_values(self, m, expected_values, buf_equals_file=True):
  173. for filename, expected_value in expected_values.items():
  174. try:
  175. filename = os.path.join(self.TESTDATA_DIR, filename)
  176. except TypeError:
  177. - filename = os.path.join(
  178. - self.TESTDATA_DIR.encode('utf-8'), filename)
  179. + filename = os.path.join(self.TESTDATA_DIR.encode("utf-8"), filename)
  180. if type(expected_value) is not tuple:
  181. expected_value = (expected_value,)
  182. - with open(filename, 'rb') as f:
  183. + with open(filename, "rb") as f:
  184. buf_value = m.from_buffer(f.read())
  185. file_value = m.from_file(filename)
  186. @@ -55,10 +56,10 @@
  187. def test_from_file_str_and_bytes(self):
  188. filename = os.path.join(self.TESTDATA_DIR, "test.pdf")
  189. - self.assertEqual('application/pdf',
  190. - magic.from_file(filename, mime=True))
  191. - self.assertEqual('application/pdf',
  192. - magic.from_file(filename.encode('utf-8'), mime=True))
  193. + self.assertEqual("application/pdf", magic.from_file(filename, mime=True))
  194. + self.assertEqual(
  195. + "application/pdf", magic.from_file(filename.encode("utf-8"), mime=True)
  196. + )
  197. def test_from_descriptor_str_and_bytes(self):
  198. if SKIP_FROM_DESCRIPTOR:
  199. @@ -66,10 +67,12 @@
  200. filename = os.path.join(self.TESTDATA_DIR, "test.pdf")
  201. with open(filename) as f:
  202. - self.assertEqual('application/pdf',
  203. - magic.from_descriptor(f.fileno(), mime=True))
  204. - self.assertEqual('application/pdf',
  205. - magic.from_descriptor(f.fileno(), mime=True))
  206. + self.assertEqual(
  207. + "application/pdf", magic.from_descriptor(f.fileno(), mime=True)
  208. + )
  209. + self.assertEqual(
  210. + "application/pdf", magic.from_descriptor(f.fileno(), mime=True)
  211. + )
  212. def test_from_buffer_str_and_bytes(self):
  213. if SKIP_FROM_DESCRIPTOR:
  214. @@ -78,125 +81,151 @@
  215. self.assertTrue(
  216. m.from_buffer('#!/usr/bin/env python\nprint("foo")')
  217. - in ("text/x-python", "text/x-script.python"))
  218. + in ("text/x-python", "text/x-script.python")
  219. + )
  220. self.assertTrue(
  221. m.from_buffer(b'#!/usr/bin/env python\nprint("foo")')
  222. - in ("text/x-python", "text/x-script.python"))
  223. + in ("text/x-python", "text/x-script.python")
  224. + )
  225. def test_mime_types(self):
  226. - dest = os.path.join(MagicTest.TESTDATA_DIR,
  227. - b'\xce\xbb'.decode('utf-8'))
  228. - shutil.copyfile(os.path.join(MagicTest.TESTDATA_DIR, 'lambda'), dest)
  229. + dest = os.path.join(MagicTest.TESTDATA_DIR, b"\xce\xbb".decode("utf-8"))
  230. + shutil.copyfile(os.path.join(MagicTest.TESTDATA_DIR, "lambda"), dest)
  231. try:
  232. m = magic.Magic(mime=True)
  233. - self.assert_values(m, {
  234. - 'magic._pyc_': ('application/octet-stream', 'text/x-bytecode.python', 'application/x-bytecode.python'),
  235. - 'test.pdf': 'application/pdf',
  236. - 'test.gz': ('application/gzip', 'application/x-gzip'),
  237. - 'test.snappy.parquet': 'application/octet-stream',
  238. - 'text.txt': 'text/plain',
  239. - b'\xce\xbb'.decode('utf-8'): 'text/plain',
  240. - b'\xce\xbb': 'text/plain',
  241. - })
  242. + self.assert_values(
  243. + m,
  244. + {
  245. + "magic._pyc_": (
  246. + "application/octet-stream",
  247. + "text/x-bytecode.python",
  248. + "application/x-bytecode.python",
  249. + ),
  250. + "test.pdf": "application/pdf",
  251. + "test.gz": ("application/gzip", "application/x-gzip"),
  252. + "test.snappy.parquet": "application/octet-stream",
  253. + "text.txt": "text/plain",
  254. + b"\xce\xbb".decode("utf-8"): "text/plain",
  255. + b"\xce\xbb": "text/plain",
  256. + },
  257. + )
  258. finally:
  259. os.unlink(dest)
  260. def test_descriptions(self):
  261. m = magic.Magic()
  262. - os.environ['TZ'] = 'UTC' # To get last modified date of test.gz in UTC
  263. + os.environ["TZ"] = "UTC" # To get last modified date of test.gz in UTC
  264. try:
  265. - self.assert_values(m, {
  266. - 'magic._pyc_': 'python 2.4 byte-compiled',
  267. - 'test.pdf': ('PDF document, version 1.2',
  268. - 'PDF document, version 1.2, 2 pages',
  269. - 'PDF document, version 1.2, 2 page(s)'),
  270. - 'test.gz':
  271. - ('gzip compressed data, was "test", from Unix, last '
  272. - 'modified: Sun Jun 29 01:32:52 2008',
  273. - 'gzip compressed data, was "test", last modified'
  274. - ': Sun Jun 29 01:32:52 2008, from Unix',
  275. - 'gzip compressed data, was "test", last modified'
  276. - ': Sun Jun 29 01:32:52 2008, from Unix, original size 15',
  277. - 'gzip compressed data, was "test", '
  278. - 'last modified: Sun Jun 29 01:32:52 2008, '
  279. - 'from Unix, original size modulo 2^32 15',
  280. - 'gzip compressed data, was "test", last modified'
  281. - ': Sun Jun 29 01:32:52 2008, from Unix, truncated'
  282. - ),
  283. - 'text.txt': 'ASCII text',
  284. - 'test.snappy.parquet': ('Apache Parquet', 'Par archive data'),
  285. - }, buf_equals_file=False)
  286. + self.assert_values(
  287. + m,
  288. + {
  289. + "magic._pyc_": "python 2.4 byte-compiled",
  290. + "test.pdf": (
  291. + "PDF document, version 1.2",
  292. + "PDF document, version 1.2, 2 pages",
  293. + "PDF document, version 1.2, 2 page(s)",
  294. + ),
  295. + "test.gz": (
  296. + 'gzip compressed data, was "test", from Unix, last '
  297. + "modified: Sun Jun 29 01:32:52 2008",
  298. + 'gzip compressed data, was "test", last modified'
  299. + ": Sun Jun 29 01:32:52 2008, from Unix",
  300. + 'gzip compressed data, was "test", last modified'
  301. + ": Sun Jun 29 01:32:52 2008, from Unix, original size 15",
  302. + 'gzip compressed data, was "test", '
  303. + "last modified: Sun Jun 29 01:32:52 2008, "
  304. + "from Unix, original size modulo 2^32 15",
  305. + 'gzip compressed data, was "test", last modified'
  306. + ": Sun Jun 29 01:32:52 2008, from Unix, truncated",
  307. + ),
  308. + "text.txt": "ASCII text",
  309. + "test.snappy.parquet": ("Apache Parquet", "Par archive data"),
  310. + },
  311. + buf_equals_file=False,
  312. + )
  313. finally:
  314. - del os.environ['TZ']
  315. + del os.environ["TZ"]
  316. def test_extension(self):
  317. try:
  318. m = magic.Magic(extension=True)
  319. - self.assert_values(m, {
  320. - # some versions return '' for the extensions of a gz file,
  321. - # including w/ the command line. Who knows...
  322. - 'test.gz': ('gz/tgz/tpz/zabw/svgz/adz/kmy/xcfgz', 'gz/tgz/tpz/zabw/svgz', '', '???'),
  323. - 'name_use.jpg': 'jpeg/jpg/jpe/jfif',
  324. - })
  325. + self.assert_values(
  326. + m,
  327. + {
  328. + # some versions return '' for the extensions of a gz file,
  329. + # including w/ the command line. Who knows...
  330. + "test.gz": (
  331. + "gz/tgz/tpz/zabw/svgz/adz/kmy/xcfgz",
  332. + "gz/tgz/tpz/zabw/svgz",
  333. + "",
  334. + "???",
  335. + ),
  336. + "name_use.jpg": "jpeg/jpg/jpe/jfif",
  337. + },
  338. + )
  339. except NotImplementedError:
  340. - self.skipTest('MAGIC_EXTENSION not supported in this version')
  341. + self.skipTest("MAGIC_EXTENSION not supported in this version")
  342. def test_unicode_result_nonraw(self):
  343. m = magic.Magic(raw=False)
  344. - src = os.path.join(MagicTest.TESTDATA_DIR, 'pgpunicode')
  345. + src = os.path.join(MagicTest.TESTDATA_DIR, "pgpunicode")
  346. result = m.from_file(src)
  347. # NOTE: This check is added as otherwise some magic files don't identify the test case as a PGP key.
  348. - if 'PGP' in result:
  349. + if "PGP" in result:
  350. assert r"PGP\011Secret Sub-key -" == result
  351. else:
  352. raise unittest.SkipTest("Magic file doesn't return expected type.")
  353. def test_unicode_result_raw(self):
  354. m = magic.Magic(raw=True)
  355. - src = os.path.join(MagicTest.TESTDATA_DIR, 'pgpunicode')
  356. + src = os.path.join(MagicTest.TESTDATA_DIR, "pgpunicode")
  357. result = m.from_file(src)
  358. - if 'PGP' in result:
  359. - assert b'PGP\tSecret Sub-key -' == result.encode('utf-8')
  360. + if "PGP" in result:
  361. + assert b"PGP\tSecret Sub-key -" == result.encode("utf-8")
  362. else:
  363. raise unittest.SkipTest("Magic file doesn't return expected type.")
  364. def test_mime_encodings(self):
  365. m = magic.Magic(mime_encoding=True)
  366. - self.assert_values(m, {
  367. - 'text-iso8859-1.txt': 'iso-8859-1',
  368. - 'text.txt': 'us-ascii',
  369. - })
  370. + self.assert_values(
  371. + m,
  372. + {
  373. + "text-iso8859-1.txt": "iso-8859-1",
  374. + "text.txt": "us-ascii",
  375. + },
  376. + )
  377. def test_errors(self):
  378. m = magic.Magic()
  379. - self.assertRaises(IOError, m.from_file, 'nonexistent')
  380. - self.assertRaises(magic.MagicException, magic.Magic,
  381. - magic_file='nonexistent')
  382. - os.environ['MAGIC'] = 'nonexistent'
  383. + self.assertRaises(IOError, m.from_file, "nonexistent")
  384. + self.assertRaises(magic.MagicException, magic.Magic, magic_file="nonexistent")
  385. + os.environ["MAGIC"] = "nonexistent"
  386. try:
  387. self.assertRaises(magic.MagicException, magic.Magic)
  388. finally:
  389. - del os.environ['MAGIC']
  390. + del os.environ["MAGIC"]
  391. def test_keep_going(self):
  392. - filename = os.path.join(self.TESTDATA_DIR, 'keep-going.jpg')
  393. + filename = os.path.join(self.TESTDATA_DIR, "keep-going.jpg")
  394. m = magic.Magic(mime=True)
  395. - self.assertEqual(m.from_file(filename), 'image/jpeg')
  396. + self.assertEqual(m.from_file(filename), "image/jpeg")
  397. try:
  398. # this will throw if you have an "old" version of the library
  399. # I'm otherwise not sure how to query if keep_going is supported
  400. magic.version()
  401. m = magic.Magic(mime=True, keep_going=True)
  402. - self.assertEqual(m.from_file(filename),
  403. - 'image/jpeg\\012- application/octet-stream')
  404. + self.assertEqual(
  405. + m.from_file(filename), "image/jpeg\\012- application/octet-stream"
  406. + )
  407. except NotImplementedError:
  408. pass
  409. def test_rethrow(self):
  410. old = magic.magic_buffer
  411. try:
  412. +
  413. def t(x, y):
  414. raise magic.MagicException("passthrough")
  415. @@ -217,16 +246,47 @@
  416. def test_name_count(self):
  417. m = magic.Magic()
  418. - with open(os.path.join(self.TESTDATA_DIR, 'name_use.jpg'), 'rb') as f:
  419. + with open(os.path.join(self.TESTDATA_DIR, "name_use.jpg"), "rb") as f:
  420. m.from_buffer(f.read())
  421. def test_pathlike(self):
  422. if sys.version_info < (3, 6):
  423. return
  424. from pathlib import Path
  425. - path = Path(self.TESTDATA_DIR, "test.pdf")
  426. +
  427. + path = Path(self.TESTDATA_DIR, "test.pdf")
  428. m = magic.Magic(mime=True)
  429. - self.assertEqual('application/pdf', m.from_file(path))
  430. + self.assertEqual("application/pdf", m.from_file(path))
  431. +
  432. + def test_symlink(self):
  433. + # TODO: 3.0
  434. + if not hasattr(tempfile, "TemporaryDirectory"):
  435. + return
  436. +
  437. + with tempfile.TemporaryDirectory() as tmp:
  438. + tmp_link = os.path.join(tmp, "test_link")
  439. + tmp_broken = os.path.join(tmp, "nonexistent")
  440. +
  441. + os.symlink(
  442. + os.path.join(self.TESTDATA_DIR, "test.pdf"),
  443. + tmp_link,
  444. + )
  445. +
  446. + os.symlink("/nonexistent", tmp_broken)
  447. +
  448. + m = magic.Magic()
  449. + m_follow = magic.Magic(follow_symlinks=True)
  450. + self.assertTrue(m.from_file(tmp_link).startswith("symbolic link to "))
  451. + self.assertTrue(m_follow.from_file(tmp_link).startswith("PDF document"))
  452. +
  453. + self.assertTrue(
  454. + m.from_file(tmp_broken).startswith(
  455. + "broken symbolic link to /nonexistent"
  456. + )
  457. + )
  458. +
  459. + self.assertRaises(IOError, m_follow.from_file, tmp_broken)
  460. +
  461. -if __name__ == '__main__':
  462. +if __name__ == "__main__":
  463. unittest.main()