Daniel Brötzmann pushed to branch sanitize-filenames at gajim / gajim

Commits:

4 changed files:

Changes:

  • gajim/common/helpers.py
    ... ... @@ -37,7 +37,6 @@
    37 37
     import re
    
    38 38
     import os
    
    39 39
     import subprocess
    
    40
    -import base64
    
    41 40
     import hashlib
    
    42 41
     import shlex
    
    43 42
     import socket
    
    ... ... @@ -58,10 +57,10 @@
    58 57
     from datetime import timedelta
    
    59 58
     from urllib.parse import unquote
    
    60 59
     from urllib.parse import urlparse
    
    61
    -from encodings.punycode import punycode_encode
    
    62 60
     from functools import wraps
    
    63 61
     from pathlib import Path
    
    64 62
     from packaging.version import Version as V
    
    63
    +import unicodedata
    
    65 64
     
    
    66 65
     from nbxmpp.namespaces import Namespace
    
    67 66
     from nbxmpp.const import Role
    
    ... ... @@ -358,26 +357,35 @@ def get_file_path_from_dnd_dropped_uri(uri: str) -> str:
    358 357
     
    
    359 358
     def sanitize_filename(filename: str) -> str:
    
    360 359
         '''
    
    361
    -    Make sure the filename we will write does contain only acceptable and latin
    
    362
    -    characters, and is not too long (in that case hash it)
    
    360
    +    Sanitize filename of elements not allowed on Windows
    
    361
    +    https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
    
    363 362
         '''
    
    364
    -    # 48 is the limit
    
    365
    -    if len(filename) > 48:
    
    366
    -        hash_ = hashlib.md5(filename.encode('utf-8'))
    
    367
    -        filename = base64.b64encode(hash_.digest()).decode('utf-8')
    
    368
    -
    
    369
    -    # make it latin chars only
    
    370
    -    filename = punycode_encode(filename).decode('utf-8')
    
    371
    -    filename = filename.replace('/', '_')
    
    372
    -    if os.name == 'nt':
    
    373
    -        filename = filename.replace('?', '_')\
    
    374
    -                           .replace(':', '_')\
    
    375
    -                           .replace('\\', '_')\
    
    376
    -                           .replace('"', "'")\
    
    377
    -                           .replace('|', '_')\
    
    378
    -                           .replace('*', '_')\
    
    379
    -                           .replace('<', '_')\
    
    380
    -                           .replace('>', '_')
    
    363
    +    blacklist = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '\0']
    
    364
    +    reserved_filenames = [
    
    365
    +        'CON', 'PRN', 'AUX', 'NUL',
    
    366
    +        'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
    
    367
    +        'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
    
    368
    +    ]
    
    369
    +    filename = ''.join(char for char in filename if char not in blacklist)
    
    370
    +
    
    371
    +    filename = ''.join(char for char in filename if 31 < ord(char))
    
    372
    +
    
    373
    +    filename = unicodedata.normalize('NFKD', filename)
    
    374
    +    filename = filename.rstrip('. ')
    
    375
    +    filename = filename.strip()
    
    376
    +
    
    377
    +    if all([char == '.' for char in filename]):
    
    378
    +        filename = f'__{filename}'
    
    379
    +    if filename in reserved_filenames:
    
    380
    +        filename = f'__{filename}'
    
    381
    +    if len(filename) == 0:
    
    382
    +        filename = '__'
    
    383
    +    if len(filename) > 50:
    
    384
    +        # Many Filesystems have a limit on filename length
    
    385
    +        # Most have 255, some encrypted ones only 143
    
    386
    +        # We add around 50 chars for the hash,
    
    387
    +        # so the filename should not exceed 50
    
    388
    +        filename = filename[:50]
    
    381 389
     
    
    382 390
         return filename
    
    383 391
     
    

  • gajim/common/modules/chatstates.py
    ... ... @@ -253,6 +253,11 @@ def set_chatstate_delayed(self,
    253 253
             # Used when we go from Composing -> Active after deleting all text
    
    254 254
             # from the Textview. We delay the Active state because maybe the
    
    255 255
             # User starts writing again.
    
    256
    +
    
    257
    +        # Don’t send chatstates to ourself
    
    258
    +        if self._con.get_own_jid().bare_match(contact.jid):
    
    259
    +            return
    
    260
    +
    
    256 261
             self.remove_delay_timeout(contact)
    
    257 262
             self._delay_timeout_ids[contact.jid] = GLib.timeout_add_seconds(
    
    258 263
                 2, self.set_chatstate, contact, state)
    

  • gajim/common/preview_helpers.py
    ... ... @@ -45,6 +45,7 @@
    45 45
     from cryptography.hazmat.primitives.ciphers import algorithms
    
    46 46
     from cryptography.hazmat.primitives.ciphers.modes import GCM
    
    47 47
     
    
    48
    +from gajim.common.helpers import sanitize_filename
    
    48 49
     from gajim.common.i18n import _
    
    49 50
     
    
    50 51
     log = logging.getLogger('gajim.c.preview_helpers')
    
    ... ... @@ -308,12 +309,7 @@ def get_image_paths(uri: str,
    308 309
         web_stem = path.stem
    
    309 310
         extension = path.suffix
    
    310 311
     
    
    311
    -    if len(web_stem) > 90:
    
    312
    -        # Many Filesystems have a limit on filename length
    
    313
    -        # Most have 255, some encrypted ones only 143
    
    314
    -        # We add around 50 chars for the hash,
    
    315
    -        # so the filename should not exceed 90
    
    316
    -        web_stem = web_stem[:90]
    
    312
    +    web_stem = sanitize_filename(web_stem)
    
    317 313
     
    
    318 314
         name_hash = hashlib.sha1(str(uri).encode()).hexdigest()
    
    319 315
     
    

  • test/no_gui/test_sanitize_filename.py
    1
    +import unittest
    
    2
    +
    
    3
    +from gajim.common.helpers import sanitize_filename
    
    4
    +
    
    5
    +
    
    6
    +class SanitizeTest(unittest.TestCase):
    
    7
    +    '''Tests for the sanitize_filename function.'''
    
    8
    +
    
    9
    +    def test_invalid_chars(self):
    
    10
    +        '''Make sure invalid characters are removed in filenames.'''
    
    11
    +        self.assertEqual(sanitize_filename('A/B/C'), 'ABC')
    
    12
    +        self.assertEqual(sanitize_filename('A*C.d'), 'AC.d')
    
    13
    +        self.assertEqual(sanitize_filename('A?C.d'), 'AC.d')
    
    14
    +
    
    15
    +    def test_invalid_suffix(self):
    
    16
    +        '''Dots are not allowed at the end.'''
    
    17
    +        self.assertEqual(sanitize_filename('def.'), 'def')
    
    18
    +        self.assertEqual(sanitize_filename('def.ghi'), 'def.ghi')
    
    19
    +        self.assertTrue(sanitize_filename('X' * 1000 + '.').endswith('X'))
    
    20
    +
    
    21
    +    def test_reserved_words(self):
    
    22
    +        '''Make sure reserved Windows words are prefixed.'''
    
    23
    +        self.assertEqual(sanitize_filename('NUL'), '__NUL')
    
    24
    +        self.assertEqual(sanitize_filename('..'), '__')
    
    25
    +
    
    26
    +    def test_long_names(self):
    
    27
    +        '''Make sure long names are truncated.'''
    
    28
    +        self.assertEqual(len(sanitize_filename('X' * 300)), 50)
    
    29
    +        self.assertEqual(len(sanitize_filename(
    
    30
    +            '.'.join(['X' * 100, 'X' * 100, 'X' * 100]))), 50)
    
    31
    +        self.assertEqual(len(sanitize_filename(
    
    32
    +            '.'.join(['X' * 300, 'X' * 300, 'X' * 300]))), 50)
    
    33
    +        self.assertEqual(len(sanitize_filename('.' * 300 + '.txt')), 50)
    
    34
    +
    
    35
    +    def test_unicode_normalization(self):
    
    36
    +        '''Names should be NFKD normalized.'''
    
    37
    +        self.assertEqual(sanitize_filename('ў'), chr(1091) + chr(774))
    
    38
    +
    
    39
    +
    
    40
    +if __name__ == '__main__':
    
    41
    +    unittest.main()