1""" 2This module implements the algorithm for converting between a "user name" - 3something that a user can choose arbitrarily inside a font editor - and a file 4name suitable for use in a wide range of operating systems and filesystems. 5 6The `UFO 3 specification <http://unifiedfontobject.org/versions/ufo3/conventions/>`_ 7provides an example of an algorithm for such conversion, which avoids illegal 8characters, reserved file names, ambiguity between upper- and lower-case 9characters, and clashes with existing files. 10 11This code was originally copied from 12`ufoLib <https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py>`_ 13by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers: 14 15- Erik van Blokland 16- Tal Leming 17- Just van Rossum 18""" 19 20 21illegalCharacters = r"\" * + / : < > ? [ \ ] | \0".split(" ") 22illegalCharacters += [chr(i) for i in range(1, 32)] 23illegalCharacters += [chr(0x7F)] 24reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") 25reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") 26maxFileNameLength = 255 27 28 29class NameTranslationError(Exception): 30 pass 31 32 33def userNameToFileName(userName, existing=[], prefix="", suffix=""): 34 """Converts from a user name to a file name. 35 36 Takes care to avoid illegal characters, reserved file names, ambiguity between 37 upper- and lower-case characters, and clashes with existing files. 38 39 Args: 40 userName (str): The input file name. 41 existing: A case-insensitive list of all existing file names. 42 prefix: Prefix to be prepended to the file name. 43 suffix: Suffix to be appended to the file name. 44 45 Returns: 46 A suitable filename. 47 48 Raises: 49 NameTranslationError: If no suitable name could be generated. 50 51 Examples:: 52 53 >>> userNameToFileName("a") == "a" 54 True 55 >>> userNameToFileName("A") == "A_" 56 True 57 >>> userNameToFileName("AE") == "A_E_" 58 True 59 >>> userNameToFileName("Ae") == "A_e" 60 True 61 >>> userNameToFileName("ae") == "ae" 62 True 63 >>> userNameToFileName("aE") == "aE_" 64 True 65 >>> userNameToFileName("a.alt") == "a.alt" 66 True 67 >>> userNameToFileName("A.alt") == "A_.alt" 68 True 69 >>> userNameToFileName("A.Alt") == "A_.A_lt" 70 True 71 >>> userNameToFileName("A.aLt") == "A_.aL_t" 72 True 73 >>> userNameToFileName(u"A.alT") == "A_.alT_" 74 True 75 >>> userNameToFileName("T_H") == "T__H_" 76 True 77 >>> userNameToFileName("T_h") == "T__h" 78 True 79 >>> userNameToFileName("t_h") == "t_h" 80 True 81 >>> userNameToFileName("F_F_I") == "F__F__I_" 82 True 83 >>> userNameToFileName("f_f_i") == "f_f_i" 84 True 85 >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" 86 True 87 >>> userNameToFileName(".notdef") == "_notdef" 88 True 89 >>> userNameToFileName("con") == "_con" 90 True 91 >>> userNameToFileName("CON") == "C_O_N_" 92 True 93 >>> userNameToFileName("con.alt") == "_con.alt" 94 True 95 >>> userNameToFileName("alt.con") == "alt._con" 96 True 97 """ 98 # the incoming name must be a str 99 if not isinstance(userName, str): 100 raise ValueError("The value for userName must be a string.") 101 # establish the prefix and suffix lengths 102 prefixLength = len(prefix) 103 suffixLength = len(suffix) 104 # replace an initial period with an _ 105 # if no prefix is to be added 106 if not prefix and userName[0] == ".": 107 userName = "_" + userName[1:] 108 # filter the user name 109 filteredUserName = [] 110 for character in userName: 111 # replace illegal characters with _ 112 if character in illegalCharacters: 113 character = "_" 114 # add _ to all non-lower characters 115 elif character != character.lower(): 116 character += "_" 117 filteredUserName.append(character) 118 userName = "".join(filteredUserName) 119 # clip to 255 120 sliceLength = maxFileNameLength - prefixLength - suffixLength 121 userName = userName[:sliceLength] 122 # test for illegal files names 123 parts = [] 124 for part in userName.split("."): 125 if part.lower() in reservedFileNames: 126 part = "_" + part 127 parts.append(part) 128 userName = ".".join(parts) 129 # test for clash 130 fullName = prefix + userName + suffix 131 if fullName.lower() in existing: 132 fullName = handleClash1(userName, existing, prefix, suffix) 133 # finished 134 return fullName 135 136def handleClash1(userName, existing=[], prefix="", suffix=""): 137 """ 138 existing should be a case-insensitive list 139 of all existing file names. 140 141 >>> prefix = ("0" * 5) + "." 142 >>> suffix = "." + ("0" * 10) 143 >>> existing = ["a" * 5] 144 145 >>> e = list(existing) 146 >>> handleClash1(userName="A" * 5, existing=e, 147 ... prefix=prefix, suffix=suffix) == ( 148 ... '00000.AAAAA000000000000001.0000000000') 149 True 150 151 >>> e = list(existing) 152 >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) 153 >>> handleClash1(userName="A" * 5, existing=e, 154 ... prefix=prefix, suffix=suffix) == ( 155 ... '00000.AAAAA000000000000002.0000000000') 156 True 157 158 >>> e = list(existing) 159 >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) 160 >>> handleClash1(userName="A" * 5, existing=e, 161 ... prefix=prefix, suffix=suffix) == ( 162 ... '00000.AAAAA000000000000001.0000000000') 163 True 164 """ 165 # if the prefix length + user name length + suffix length + 15 is at 166 # or past the maximum length, silce 15 characters off of the user name 167 prefixLength = len(prefix) 168 suffixLength = len(suffix) 169 if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: 170 l = (prefixLength + len(userName) + suffixLength + 15) 171 sliceLength = maxFileNameLength - l 172 userName = userName[:sliceLength] 173 finalName = None 174 # try to add numbers to create a unique name 175 counter = 1 176 while finalName is None: 177 name = userName + str(counter).zfill(15) 178 fullName = prefix + name + suffix 179 if fullName.lower() not in existing: 180 finalName = fullName 181 break 182 else: 183 counter += 1 184 if counter >= 999999999999999: 185 break 186 # if there is a clash, go to the next fallback 187 if finalName is None: 188 finalName = handleClash2(existing, prefix, suffix) 189 # finished 190 return finalName 191 192def handleClash2(existing=[], prefix="", suffix=""): 193 """ 194 existing should be a case-insensitive list 195 of all existing file names. 196 197 >>> prefix = ("0" * 5) + "." 198 >>> suffix = "." + ("0" * 10) 199 >>> existing = [prefix + str(i) + suffix for i in range(100)] 200 201 >>> e = list(existing) 202 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 203 ... '00000.100.0000000000') 204 True 205 206 >>> e = list(existing) 207 >>> e.remove(prefix + "1" + suffix) 208 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 209 ... '00000.1.0000000000') 210 True 211 212 >>> e = list(existing) 213 >>> e.remove(prefix + "2" + suffix) 214 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 215 ... '00000.2.0000000000') 216 True 217 """ 218 # calculate the longest possible string 219 maxLength = maxFileNameLength - len(prefix) - len(suffix) 220 maxValue = int("9" * maxLength) 221 # try to find a number 222 finalName = None 223 counter = 1 224 while finalName is None: 225 fullName = prefix + str(counter) + suffix 226 if fullName.lower() not in existing: 227 finalName = fullName 228 break 229 else: 230 counter += 1 231 if counter >= maxValue: 232 break 233 # raise an error if nothing has been found 234 if finalName is None: 235 raise NameTranslationError("No unique name could be found.") 236 # finished 237 return finalName 238 239if __name__ == "__main__": 240 import doctest 241 import sys 242 sys.exit(doctest.testmod().failed) 243