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