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 20illegalCharacters = r"\" * + / : < > ? [ \ ] | \0".split(" ") 21illegalCharacters += [chr(i) for i in range(1, 32)] 22illegalCharacters += [chr(0x7F)] 23reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") 24reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") 25maxFileNameLength = 255 26 27 28class NameTranslationError(Exception): 29 pass 30 31 32def userNameToFileName(userName, existing=[], prefix="", suffix=""): 33 """Converts from a user name to a file name. 34 35 Takes care to avoid illegal characters, reserved file names, ambiguity between 36 upper- and lower-case characters, and clashes with existing files. 37 38 Args: 39 userName (str): The input file name. 40 existing: A case-insensitive list of all existing file names. 41 prefix: Prefix to be prepended to the file name. 42 suffix: Suffix to be appended to the file name. 43 44 Returns: 45 A suitable filename. 46 47 Raises: 48 NameTranslationError: If no suitable name could be generated. 49 50 Examples:: 51 52 >>> userNameToFileName("a") == "a" 53 True 54 >>> userNameToFileName("A") == "A_" 55 True 56 >>> userNameToFileName("AE") == "A_E_" 57 True 58 >>> userNameToFileName("Ae") == "A_e" 59 True 60 >>> userNameToFileName("ae") == "ae" 61 True 62 >>> userNameToFileName("aE") == "aE_" 63 True 64 >>> userNameToFileName("a.alt") == "a.alt" 65 True 66 >>> userNameToFileName("A.alt") == "A_.alt" 67 True 68 >>> userNameToFileName("A.Alt") == "A_.A_lt" 69 True 70 >>> userNameToFileName("A.aLt") == "A_.aL_t" 71 True 72 >>> userNameToFileName(u"A.alT") == "A_.alT_" 73 True 74 >>> userNameToFileName("T_H") == "T__H_" 75 True 76 >>> userNameToFileName("T_h") == "T__h" 77 True 78 >>> userNameToFileName("t_h") == "t_h" 79 True 80 >>> userNameToFileName("F_F_I") == "F__F__I_" 81 True 82 >>> userNameToFileName("f_f_i") == "f_f_i" 83 True 84 >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" 85 True 86 >>> userNameToFileName(".notdef") == "_notdef" 87 True 88 >>> userNameToFileName("con") == "_con" 89 True 90 >>> userNameToFileName("CON") == "C_O_N_" 91 True 92 >>> userNameToFileName("con.alt") == "_con.alt" 93 True 94 >>> userNameToFileName("alt.con") == "alt._con" 95 True 96 """ 97 # the incoming name must be a str 98 if not isinstance(userName, str): 99 raise ValueError("The value for userName must be a string.") 100 # establish the prefix and suffix lengths 101 prefixLength = len(prefix) 102 suffixLength = len(suffix) 103 # replace an initial period with an _ 104 # if no prefix is to be added 105 if not prefix and userName[0] == ".": 106 userName = "_" + userName[1:] 107 # filter the user name 108 filteredUserName = [] 109 for character in userName: 110 # replace illegal characters with _ 111 if character in illegalCharacters: 112 character = "_" 113 # add _ to all non-lower characters 114 elif character != character.lower(): 115 character += "_" 116 filteredUserName.append(character) 117 userName = "".join(filteredUserName) 118 # clip to 255 119 sliceLength = maxFileNameLength - prefixLength - suffixLength 120 userName = userName[:sliceLength] 121 # test for illegal files names 122 parts = [] 123 for part in userName.split("."): 124 if part.lower() in reservedFileNames: 125 part = "_" + part 126 parts.append(part) 127 userName = ".".join(parts) 128 # test for clash 129 fullName = prefix + userName + suffix 130 if fullName.lower() in existing: 131 fullName = handleClash1(userName, existing, prefix, suffix) 132 # finished 133 return fullName 134 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 192 193def handleClash2(existing=[], prefix="", suffix=""): 194 """ 195 existing should be a case-insensitive list 196 of all existing file names. 197 198 >>> prefix = ("0" * 5) + "." 199 >>> suffix = "." + ("0" * 10) 200 >>> existing = [prefix + str(i) + suffix for i in range(100)] 201 202 >>> e = list(existing) 203 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 204 ... '00000.100.0000000000') 205 True 206 207 >>> e = list(existing) 208 >>> e.remove(prefix + "1" + suffix) 209 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 210 ... '00000.1.0000000000') 211 True 212 213 >>> e = list(existing) 214 >>> e.remove(prefix + "2" + suffix) 215 >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( 216 ... '00000.2.0000000000') 217 True 218 """ 219 # calculate the longest possible string 220 maxLength = maxFileNameLength - len(prefix) - len(suffix) 221 maxValue = int("9" * maxLength) 222 # try to find a number 223 finalName = None 224 counter = 1 225 while finalName is None: 226 fullName = prefix + str(counter) + suffix 227 if fullName.lower() not in existing: 228 finalName = fullName 229 break 230 else: 231 counter += 1 232 if counter >= maxValue: 233 break 234 # raise an error if nothing has been found 235 if finalName is None: 236 raise NameTranslationError("No unique name could be found.") 237 # finished 238 return finalName 239 240 241if __name__ == "__main__": 242 import doctest 243 import sys 244 245 sys.exit(doctest.testmod().failed) 246