Repository navigation
Expand file tree
/
Copy pathmimetypes.py
More file actions
292 lines (254 loc) · 10.9 KB
/
Copy pathmimetypes.py
File metadata and controls
292 lines (254 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# -*- coding: utf-8 -*-
"""
Mimetypes-related utilities
# TODO: reexport stdlib mimetypes?
"""
import collections
import functools
import io
import logging
import mimetypes
import re
import zipfile
__all__ = ['guess_mimetype']
_logger = logging.getLogger(__name__)
_logger_guess_mimetype = _logger.getChild('guess_mimetype')
MIMETYPE_HEAD_SIZE = 2048
# We define our own guess_mimetype implementation and if magic is available we
# use it instead.
# discriminants for zip-based file formats
_ooxml_dirs = {
'word/': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pt/': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xl/': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
def _check_ooxml(data):
with io.BytesIO(data) as f, zipfile.ZipFile(f) as z:
filenames = z.namelist()
# OOXML documents should have a [Content_Types].xml file for early
# check that we're interested in this thing at all
if '[Content_Types].xml' not in filenames:
return False
# then there is a directory whose name denotes the type of the file:
# word, pt (powerpoint) or xl (excel)
for dirname, mime in _ooxml_dirs.items():
if any(entry.startswith(dirname) for entry in filenames):
return mime
return False
# checks that a string looks kinda sorta like a mimetype
_mime_validator = re.compile(r"""
[\w-]+ # type-name
/ # subtype separator
[\w-]+ # registration facet or subtype
(?:\.[\w-]+)* # optional faceted name
(?:\+[\w-]+)? # optional structured syntax specifier
""", re.VERBOSE)
def _check_open_container_format(data):
# Open Document Format for Office Applications (OpenDocument) Version 1.2
#
# Part 3: Packages
# 3 Packages
# 3.3 MIME Media Type
with io.BytesIO(data) as f, zipfile.ZipFile(f) as z:
# If a MIME media type for a document exists, then an OpenDocument
# package should contain a file with name "mimetype".
if 'mimetype' not in z.namelist():
return False
# The content of this file shall be the ASCII encoded MIME media type
# associated with the document.
marcel = z.read('mimetype').decode('ascii')
# check that it's not too long (RFC6838 § 4.2 restricts type and
# subtype to 127 characters each + separator, strongly recommends
# limiting them to 64 but does not require it) and that it looks a lot
# like a valid mime type
if len(marcel) < 256 and _mime_validator.match(marcel):
return marcel
return False
_old_ms_office_mimetypes = {
'.doc': 'application/msword',
'.xls': 'application/vnd.ms-excel',
'.ppt': 'application/vnd.ms-powerpoint',
}
_olecf_mimetypes = ('application/x-ole-storage', 'application/CDFV2')
_xls_pattern = re.compile(b"""
\x09\x08\x10\x00\x00\x06\x05\x00
| \xFD\xFF\xFF\xFF(\x10|\x1F|\x20|"|\\#|\\(|\\))
""", re.VERBOSE)
_ppt_pattern = re.compile(b"""
\x00\x6E\x1E\xF0
| \x0F\x00\xE8\x03
| \xA0\x46\x1D\xF0
| \xFD\xFF\xFF\xFF(\x0E|\x1C|\x43)\x00\x00\x00
""", re.VERBOSE)
def _check_olecf(data):
""" Pre-OOXML Office formats are OLE Compound Files which all use the same
file signature ("magic bytes") and should have a subheader at offset 512
(0x200).
Subheaders taken from http://www.garykessler.net/library/file_sigs.html
according to which Mac office files *may* have different subheaders. We'll
ignore that.
"""
offset = 0x200
if data.startswith(b'\xEC\xA5\xC1\x00', offset):
return 'application/msword'
# the _xls_pattern stuff doesn't seem to work correctly (the test file
# only has a bunch of \xf* at offset 0x200), that apparently works
elif b'Microsoft Excel' in data:
return 'application/vnd.ms-excel'
elif _ppt_pattern.match(data, offset):
return 'application/vnd.ms-powerpoint'
return False
def _check_svg(data):
"""This simply checks the existence of the opening and ending SVG tags"""
if b'<svg' in data and b'/svg' in data:
return 'image/svg+xml'
def _check_webp(data):
"""This checks the presence of the WEBP and VP8 in the RIFF"""
if data[8:15] == b'WEBPVP8':
return 'image/webp'
# for "master" formats with many subformats, discriminants is a list of
# functions, tried in order and the first non-falsy value returned is the
# selected mime type. If all functions return falsy values, the master
# mimetype is returned.
_Entry = collections.namedtuple('_Entry', ['mimetype', 'signatures', 'discriminants'])
_mime_mappings = (
# pdf
_Entry('application/pdf', [b'%PDF'], []),
# jpg, jpeg, png, gif, bmp, jfif
_Entry('image/jpeg', [b'\xFF\xD8\xFF\xE0', b'\xFF\xD8\xFF\xE2', b'\xFF\xD8\xFF\xE3', b'\xFF\xD8\xFF\xE1', b'\xFF\xD8\xFF\xDB'], []),
_Entry('image/png', [b'\x89PNG\r\n\x1A\n'], []),
_Entry('image/gif', [b'GIF87a', b'GIF89a'], []),
_Entry('image/bmp', [b'BM'], []),
_Entry('text/xml', [b'<'], [
_check_svg,
]),
_Entry('image/x-icon', [b'\x00\x00\x01\x00'], []),
_Entry('image/webp', [b'RIFF'], [
_check_webp,
]),
# OLECF files in general (Word, Excel, PPT, default to word because why not?)
_Entry('application/msword', [b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', b'\x0D\x44\x4F\x43'], [
_check_olecf
]),
# zip, but will include jar, odt, ods, odp, docx, xlsx, pptx, apk
_Entry('application/zip', [b'PK\x03\x04'], [_check_ooxml, _check_open_container_format]),
)
def _odoo_guess_mimetype(bin_data: bytes, default='application/octet-stream'):
""" Attempts to guess the mime type of the provided binary data, similar
to but significantly more limited than libmagic
:param bin_data: binary data to try and guess a mime type for
:returns: matched mimetype or ``application/octet-stream`` if none matched
"""
# by default, guess the type using the magic number of file hex signature (like magic, but more limited)
# see http://www.filesignatures.net/ for file signatures
for entry in _mime_mappings:
for signature in entry.signatures:
if bin_data.startswith(signature):
for discriminant in entry.discriminants:
try:
guess = discriminant(bin_data)
if guess: return guess
except Exception:
# log-and-next
_logger_guess_mimetype.warning(
"Sub-checker '%s' of type '%s' failed",
discriminant.__name__, entry.mimetype,
exc_info=True
)
# if no discriminant or no discriminant matches, return
# primary mime type
return entry.mimetype
try:
if bin_data and all(c >= ' ' or c in '\t\n\r' for c in bin_data[:1024].decode()):
return 'text/plain'
except ValueError:
pass
return default
try:
import magic
def guess_mimetype(bin_data, default=None):
if isinstance(bin_data, bytearray):
bin_data = bytes(bin_data[:MIMETYPE_HEAD_SIZE])
elif not isinstance(bin_data, bytes):
raise TypeError('`bin_data` must be bytes or bytearray')
mimetype = magic.from_buffer(bin_data[:MIMETYPE_HEAD_SIZE], mime=True)
if mimetype in ('application/CDFV2', 'application/x-ole-storage'):
# Those are the generic file format that Microsoft Office
# was using before 2006, use our own check to further
# discriminate the mimetype.
try:
if msoffice_mimetype := _check_olecf(bin_data):
return msoffice_mimetype
except Exception: # noqa: BLE001
_logger_guess_mimetype.warning(
"Sub-checker '_check_olecf' of type '%s' failed",
mimetype,
exc_info=True,
)
if mimetype == 'application/zip':
# magic doesn't properly detect some Microsoft Office
# documents created after 2025, use our own check to further
# discriminate the mimetype.
# /!\ Only work when bin_data holds the whole zipfile. /!\
try:
if msoffice_mimetype := _check_ooxml(bin_data):
return msoffice_mimetype
except zipfile.BadZipFile:
pass
except Exception: # noqa: BLE001
_logger_guess_mimetype.warning(
"Sub-checker '_check_ooxml' of type '%s' failed",
mimetype,
exc_info=True,
)
return mimetype
except ImportError:
guess_mimetype = _odoo_guess_mimetype
def neuter_mimetype(mimetype, user):
wrong_type = 'ht' in mimetype or 'xml' in mimetype or 'svg' in mimetype
if wrong_type and not user._is_system():
return 'text/plain'
return mimetype
_extension_pattern = re.compile(r'\w+')
def get_extension(filename):
# A file has no extension if it has no dot (ignoring the leading one
# of hidden files) or that what follow the last dot is not a single
# word, e.g. "Mr. Doe"
_stem, dot, ext = filename.lstrip('.').rpartition('.')
if not dot or not _extension_pattern.fullmatch(ext):
return ''
# Assume all 4-chars extensions to be valid extensions even if it is
# not known from the mimetypes database. In /etc/mime.types, only 7%
# known extensions are longer.
if len(ext) <= 4:
return f'.{ext}'.lower()
# Use the mimetype database to determine the extension of the file.
guessed_mimetype, guessed_ext = mimetypes.guess_type(filename)
if guessed_ext:
return guessed_ext
if guessed_mimetype:
return f'.{ext}'.lower()
# Unknown extension.
return ''
def fix_filename_extension(filename, mimetype):
"""
Make sure the filename ends with an extension of the mimetype.
:param str filename: the filename with an unsafe extension
:param str mimetype: the mimetype detected reading the file's content
:returns: the same filename if its extension matches the detected
mimetype, otherwise the same filename with the mimetype's
extension added at the end.
"""
extension_mimetype = mimetypes.guess_type(filename)[0]
if extension_mimetype == mimetype:
return filename
extension = get_extension(filename)
if mimetype in _olecf_mimetypes and extension in _old_ms_office_mimetypes:
return filename
if mimetype == 'application/zip' and extension in {'.docx', '.xlsx', '.pptx'}:
return filename
if extension := mimetypes.guess_extension(mimetype):
_logger.warning("File %r has an invalid extension for mimetype %r, adding %r", filename, mimetype, extension)
return filename + extension
_logger.warning("File %r has an unknown extension for mimetype %r", filename, mimetype)
return filename