xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/filenames.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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