649 lines
20 KiB
Python
649 lines
20 KiB
Python
"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts.
|
|
|
|
Functions for reading and writing raw Type 1 data:
|
|
|
|
read(path)
|
|
reads any Type 1 font file, returns the raw data and a type indicator:
|
|
'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed
|
|
to by 'path'.
|
|
Raises an error when the file does not contain valid Type 1 data.
|
|
|
|
write(path, data, kind='OTHER', dohex=False)
|
|
writes raw Type 1 data to the file pointed to by 'path'.
|
|
'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'.
|
|
'dohex' is a flag which determines whether the eexec encrypted
|
|
part should be written as hexadecimal or binary, but only if kind
|
|
is 'OTHER'.
|
|
"""
|
|
|
|
import fontTools
|
|
from fontTools.misc import eexec
|
|
from fontTools.misc.macCreatorType import getMacCreatorAndType
|
|
from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
|
|
from fontTools.misc.psOperators import (
|
|
_type1_pre_eexec_order,
|
|
_type1_fontinfo_order,
|
|
_type1_post_eexec_order,
|
|
)
|
|
from fontTools.encodings.StandardEncoding import StandardEncoding
|
|
import os
|
|
import re
|
|
|
|
__author__ = "jvr"
|
|
__version__ = "1.0b3"
|
|
DEBUG = 0
|
|
|
|
|
|
try:
|
|
try:
|
|
from Carbon import Res
|
|
except ImportError:
|
|
import Res # MacPython < 2.2
|
|
except ImportError:
|
|
haveMacSupport = 0
|
|
else:
|
|
haveMacSupport = 1
|
|
|
|
|
|
class T1Error(Exception):
|
|
pass
|
|
|
|
|
|
class T1Font(object):
|
|
"""Type 1 font class.
|
|
|
|
Uses a minimal interpeter that supports just about enough PS to parse
|
|
Type 1 fonts.
|
|
"""
|
|
|
|
def __init__(self, path, encoding="ascii", kind=None):
|
|
if kind is None:
|
|
self.data, _ = read(path)
|
|
elif kind == "LWFN":
|
|
self.data = readLWFN(path)
|
|
elif kind == "PFB":
|
|
self.data = readPFB(path)
|
|
elif kind == "OTHER":
|
|
self.data = readOther(path)
|
|
else:
|
|
raise ValueError(kind)
|
|
self.encoding = encoding
|
|
|
|
def saveAs(self, path, type, dohex=False):
|
|
write(path, self.getData(), type, dohex)
|
|
|
|
def getData(self):
|
|
if not hasattr(self, "data"):
|
|
self.data = self.createData()
|
|
return self.data
|
|
|
|
def getGlyphSet(self):
|
|
"""Return a generic GlyphSet, which is a dict-like object
|
|
mapping glyph names to glyph objects. The returned glyph objects
|
|
have a .draw() method that supports the Pen protocol, and will
|
|
have an attribute named 'width', but only *after* the .draw() method
|
|
has been called.
|
|
|
|
In the case of Type 1, the GlyphSet is simply the CharStrings dict.
|
|
"""
|
|
return self["CharStrings"]
|
|
|
|
def __getitem__(self, key):
|
|
if not hasattr(self, "font"):
|
|
self.parse()
|
|
return self.font[key]
|
|
|
|
def parse(self):
|
|
from fontTools.misc import psLib
|
|
from fontTools.misc import psCharStrings
|
|
|
|
self.font = psLib.suckfont(self.data, self.encoding)
|
|
charStrings = self.font["CharStrings"]
|
|
lenIV = self.font["Private"].get("lenIV", 4)
|
|
assert lenIV >= 0
|
|
subrs = self.font["Private"]["Subrs"]
|
|
for glyphName, charString in charStrings.items():
|
|
charString, R = eexec.decrypt(charString, 4330)
|
|
charStrings[glyphName] = psCharStrings.T1CharString(
|
|
charString[lenIV:], subrs=subrs
|
|
)
|
|
for i in range(len(subrs)):
|
|
charString, R = eexec.decrypt(subrs[i], 4330)
|
|
subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
|
|
del self.data
|
|
|
|
def createData(self):
|
|
sf = self.font
|
|
|
|
eexec_began = False
|
|
eexec_dict = {}
|
|
lines = []
|
|
lines.extend(
|
|
[
|
|
self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"),
|
|
self._tobytes(f"%t1Font: ({fontTools.version})"),
|
|
self._tobytes(f"%%BeginResource: font {sf['FontName']}"),
|
|
]
|
|
)
|
|
# follow t1write.c:writeRegNameKeyedFont
|
|
size = 3 # Headroom for new key addition
|
|
size += 1 # FontMatrix is always counted
|
|
size += 1 + 1 # Private, CharStings
|
|
for key in font_dictionary_keys:
|
|
size += int(key in sf)
|
|
lines.append(self._tobytes(f"{size} dict dup begin"))
|
|
|
|
for key, value in sf.items():
|
|
if eexec_began:
|
|
eexec_dict[key] = value
|
|
continue
|
|
|
|
if key == "FontInfo":
|
|
fi = sf["FontInfo"]
|
|
# follow t1write.c:writeFontInfoDict
|
|
size = 3 # Headroom for new key addition
|
|
for subkey in FontInfo_dictionary_keys:
|
|
size += int(subkey in fi)
|
|
lines.append(self._tobytes(f"/FontInfo {size} dict dup begin"))
|
|
|
|
for subkey, subvalue in fi.items():
|
|
lines.extend(self._make_lines(subkey, subvalue))
|
|
lines.append(b"end def")
|
|
elif key in _type1_post_eexec_order: # usually 'Private'
|
|
eexec_dict[key] = value
|
|
eexec_began = True
|
|
else:
|
|
lines.extend(self._make_lines(key, value))
|
|
lines.append(b"end")
|
|
eexec_portion = self.encode_eexec(eexec_dict)
|
|
lines.append(bytesjoin([b"currentfile eexec ", eexec_portion]))
|
|
|
|
for _ in range(8):
|
|
lines.append(self._tobytes("0" * 64))
|
|
lines.extend([b"cleartomark", b"%%EndResource", b"%%EOF"])
|
|
|
|
data = bytesjoin(lines, "\n")
|
|
return data
|
|
|
|
def encode_eexec(self, eexec_dict):
|
|
lines = []
|
|
|
|
# '-|', '|-', '|'
|
|
RD_key, ND_key, NP_key = None, None, None
|
|
lenIV = 4
|
|
subrs = std_subrs
|
|
|
|
# Ensure we look at Private first, because we need RD_key, ND_key, NP_key and lenIV
|
|
sortedItems = sorted(eexec_dict.items(), key=lambda item: item[0] != "Private")
|
|
|
|
for key, value in sortedItems:
|
|
if key == "Private":
|
|
pr = eexec_dict["Private"]
|
|
# follow t1write.c:writePrivateDict
|
|
size = 3 # for RD, ND, NP
|
|
for subkey in Private_dictionary_keys:
|
|
size += int(subkey in pr)
|
|
lines.append(b"dup /Private")
|
|
lines.append(self._tobytes(f"{size} dict dup begin"))
|
|
for subkey, subvalue in pr.items():
|
|
if not RD_key and subvalue == RD_value:
|
|
RD_key = subkey
|
|
elif not ND_key and subvalue in ND_values:
|
|
ND_key = subkey
|
|
elif not NP_key and subvalue in PD_values:
|
|
NP_key = subkey
|
|
|
|
if subkey == "lenIV":
|
|
lenIV = subvalue
|
|
|
|
if subkey == "OtherSubrs":
|
|
# XXX: assert that no flex hint is used
|
|
lines.append(self._tobytes(hintothers))
|
|
elif subkey == "Subrs":
|
|
for subr_bin in subvalue:
|
|
subr_bin.compile()
|
|
subrs = [subr_bin.bytecode for subr_bin in subvalue]
|
|
lines.append(f"/Subrs {len(subrs)} array".encode("ascii"))
|
|
for i, subr_bin in enumerate(subrs):
|
|
encrypted_subr, R = eexec.encrypt(
|
|
bytesjoin([char_IV[:lenIV], subr_bin]), 4330
|
|
)
|
|
lines.append(
|
|
bytesjoin(
|
|
[
|
|
self._tobytes(
|
|
f"dup {i} {len(encrypted_subr)} {RD_key} "
|
|
),
|
|
encrypted_subr,
|
|
self._tobytes(f" {NP_key}"),
|
|
]
|
|
)
|
|
)
|
|
lines.append(b"def")
|
|
|
|
lines.append(b"put")
|
|
else:
|
|
lines.extend(self._make_lines(subkey, subvalue))
|
|
elif key == "CharStrings":
|
|
lines.append(b"dup /CharStrings")
|
|
lines.append(
|
|
self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin")
|
|
)
|
|
for glyph_name, char_bin in eexec_dict["CharStrings"].items():
|
|
char_bin.compile()
|
|
encrypted_char, R = eexec.encrypt(
|
|
bytesjoin([char_IV[:lenIV], char_bin.bytecode]), 4330
|
|
)
|
|
lines.append(
|
|
bytesjoin(
|
|
[
|
|
self._tobytes(
|
|
f"/{glyph_name} {len(encrypted_char)} {RD_key} "
|
|
),
|
|
encrypted_char,
|
|
self._tobytes(f" {ND_key}"),
|
|
]
|
|
)
|
|
)
|
|
lines.append(b"end put")
|
|
else:
|
|
lines.extend(self._make_lines(key, value))
|
|
|
|
lines.extend(
|
|
[
|
|
b"end",
|
|
b"dup /FontName get exch definefont pop",
|
|
b"mark",
|
|
b"currentfile closefile\n",
|
|
]
|
|
)
|
|
|
|
eexec_portion = bytesjoin(lines, "\n")
|
|
encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665)
|
|
|
|
return encrypted_eexec
|
|
|
|
def _make_lines(self, key, value):
|
|
if key == "FontName":
|
|
return [self._tobytes(f"/{key} /{value} def")]
|
|
if key in ["isFixedPitch", "ForceBold", "RndStemUp"]:
|
|
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
|
|
elif key == "Encoding":
|
|
if value == StandardEncoding:
|
|
return [self._tobytes(f"/{key} StandardEncoding def")]
|
|
else:
|
|
# follow fontTools.misc.psOperators._type1_Encoding_repr
|
|
lines = []
|
|
lines.append(b"/Encoding 256 array")
|
|
lines.append(b"0 1 255 {1 index exch /.notdef put} for")
|
|
for i in range(256):
|
|
name = value[i]
|
|
if name != ".notdef":
|
|
lines.append(self._tobytes(f"dup {i} /{name} put"))
|
|
lines.append(b"def")
|
|
return lines
|
|
if isinstance(value, str):
|
|
return [self._tobytes(f"/{key} ({value}) def")]
|
|
elif isinstance(value, bool):
|
|
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
|
|
elif isinstance(value, list):
|
|
return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")]
|
|
elif isinstance(value, tuple):
|
|
return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")]
|
|
else:
|
|
return [self._tobytes(f"/{key} {value} def")]
|
|
|
|
def _tobytes(self, s, errors="strict"):
|
|
return tobytes(s, self.encoding, errors)
|
|
|
|
|
|
# low level T1 data read and write functions
|
|
|
|
|
|
def read(path, onlyHeader=False):
|
|
"""reads any Type 1 font file, returns raw data"""
|
|
_, ext = os.path.splitext(path)
|
|
ext = ext.lower()
|
|
creator, typ = getMacCreatorAndType(path)
|
|
if typ == "LWFN":
|
|
return readLWFN(path, onlyHeader), "LWFN"
|
|
if ext == ".pfb":
|
|
return readPFB(path, onlyHeader), "PFB"
|
|
else:
|
|
return readOther(path), "OTHER"
|
|
|
|
|
|
def write(path, data, kind="OTHER", dohex=False):
|
|
assertType1(data)
|
|
kind = kind.upper()
|
|
try:
|
|
os.remove(path)
|
|
except os.error:
|
|
pass
|
|
err = 1
|
|
try:
|
|
if kind == "LWFN":
|
|
writeLWFN(path, data)
|
|
elif kind == "PFB":
|
|
writePFB(path, data)
|
|
else:
|
|
writeOther(path, data, dohex)
|
|
err = 0
|
|
finally:
|
|
if err and not DEBUG:
|
|
try:
|
|
os.remove(path)
|
|
except os.error:
|
|
pass
|
|
|
|
|
|
# -- internal --
|
|
|
|
LWFNCHUNKSIZE = 2000
|
|
HEXLINELENGTH = 80
|
|
|
|
|
|
def readLWFN(path, onlyHeader=False):
|
|
"""reads an LWFN font file, returns raw data"""
|
|
from fontTools.misc.macRes import ResourceReader
|
|
|
|
reader = ResourceReader(path)
|
|
try:
|
|
data = []
|
|
for res in reader.get("POST", []):
|
|
code = byteord(res.data[0])
|
|
if byteord(res.data[1]) != 0:
|
|
raise T1Error("corrupt LWFN file")
|
|
if code in [1, 2]:
|
|
if onlyHeader and code == 2:
|
|
break
|
|
data.append(res.data[2:])
|
|
elif code in [3, 5]:
|
|
break
|
|
elif code == 4:
|
|
with open(path, "rb") as f:
|
|
data.append(f.read())
|
|
elif code == 0:
|
|
pass # comment, ignore
|
|
else:
|
|
raise T1Error("bad chunk code: " + repr(code))
|
|
finally:
|
|
reader.close()
|
|
data = bytesjoin(data)
|
|
assertType1(data)
|
|
return data
|
|
|
|
|
|
def readPFB(path, onlyHeader=False):
|
|
"""reads a PFB font file, returns raw data"""
|
|
data = []
|
|
with open(path, "rb") as f:
|
|
while True:
|
|
if f.read(1) != bytechr(128):
|
|
raise T1Error("corrupt PFB file")
|
|
code = byteord(f.read(1))
|
|
if code in [1, 2]:
|
|
chunklen = stringToLong(f.read(4))
|
|
chunk = f.read(chunklen)
|
|
assert len(chunk) == chunklen
|
|
data.append(chunk)
|
|
elif code == 3:
|
|
break
|
|
else:
|
|
raise T1Error("bad chunk code: " + repr(code))
|
|
if onlyHeader:
|
|
break
|
|
data = bytesjoin(data)
|
|
assertType1(data)
|
|
return data
|
|
|
|
|
|
def readOther(path):
|
|
"""reads any (font) file, returns raw data"""
|
|
with open(path, "rb") as f:
|
|
data = f.read()
|
|
assertType1(data)
|
|
chunks = findEncryptedChunks(data)
|
|
data = []
|
|
for isEncrypted, chunk in chunks:
|
|
if isEncrypted and isHex(chunk[:4]):
|
|
data.append(deHexString(chunk))
|
|
else:
|
|
data.append(chunk)
|
|
return bytesjoin(data)
|
|
|
|
|
|
# file writing tools
|
|
|
|
|
|
def writeLWFN(path, data):
|
|
# Res.FSpCreateResFile was deprecated in OS X 10.5
|
|
Res.FSpCreateResFile(path, "just", "LWFN", 0)
|
|
resRef = Res.FSOpenResFile(path, 2) # write-only
|
|
try:
|
|
Res.UseResFile(resRef)
|
|
resID = 501
|
|
chunks = findEncryptedChunks(data)
|
|
for isEncrypted, chunk in chunks:
|
|
if isEncrypted:
|
|
code = 2
|
|
else:
|
|
code = 1
|
|
while chunk:
|
|
res = Res.Resource(bytechr(code) + "\0" + chunk[: LWFNCHUNKSIZE - 2])
|
|
res.AddResource("POST", resID, "")
|
|
chunk = chunk[LWFNCHUNKSIZE - 2 :]
|
|
resID = resID + 1
|
|
res = Res.Resource(bytechr(5) + "\0")
|
|
res.AddResource("POST", resID, "")
|
|
finally:
|
|
Res.CloseResFile(resRef)
|
|
|
|
|
|
def writePFB(path, data):
|
|
chunks = findEncryptedChunks(data)
|
|
with open(path, "wb") as f:
|
|
for isEncrypted, chunk in chunks:
|
|
if isEncrypted:
|
|
code = 2
|
|
else:
|
|
code = 1
|
|
f.write(bytechr(128) + bytechr(code))
|
|
f.write(longToString(len(chunk)))
|
|
f.write(chunk)
|
|
f.write(bytechr(128) + bytechr(3))
|
|
|
|
|
|
def writeOther(path, data, dohex=False):
|
|
chunks = findEncryptedChunks(data)
|
|
with open(path, "wb") as f:
|
|
hexlinelen = HEXLINELENGTH // 2
|
|
for isEncrypted, chunk in chunks:
|
|
if isEncrypted:
|
|
code = 2
|
|
else:
|
|
code = 1
|
|
if code == 2 and dohex:
|
|
while chunk:
|
|
f.write(eexec.hexString(chunk[:hexlinelen]))
|
|
f.write(b"\r")
|
|
chunk = chunk[hexlinelen:]
|
|
else:
|
|
f.write(chunk)
|
|
|
|
|
|
# decryption tools
|
|
|
|
EEXECBEGIN = b"currentfile eexec"
|
|
# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
|
|
# follow eexec
|
|
EEXECEND = re.compile(b"(0[ \t\r\n]*){512}", flags=re.M)
|
|
EEXECINTERNALEND = b"currentfile closefile"
|
|
EEXECBEGINMARKER = b"%-- eexec start\r"
|
|
EEXECENDMARKER = b"%-- eexec end\r"
|
|
|
|
_ishexRE = re.compile(b"[0-9A-Fa-f]*$")
|
|
|
|
|
|
def isHex(text):
|
|
return _ishexRE.match(text) is not None
|
|
|
|
|
|
def decryptType1(data):
|
|
chunks = findEncryptedChunks(data)
|
|
data = []
|
|
for isEncrypted, chunk in chunks:
|
|
if isEncrypted:
|
|
if isHex(chunk[:4]):
|
|
chunk = deHexString(chunk)
|
|
decrypted, R = eexec.decrypt(chunk, 55665)
|
|
decrypted = decrypted[4:]
|
|
if (
|
|
decrypted[-len(EEXECINTERNALEND) - 1 : -1] != EEXECINTERNALEND
|
|
and decrypted[-len(EEXECINTERNALEND) - 2 : -2] != EEXECINTERNALEND
|
|
):
|
|
raise T1Error("invalid end of eexec part")
|
|
decrypted = decrypted[: -len(EEXECINTERNALEND) - 2] + b"\r"
|
|
data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
|
|
else:
|
|
if chunk[-len(EEXECBEGIN) - 1 : -1] == EEXECBEGIN:
|
|
data.append(chunk[: -len(EEXECBEGIN) - 1])
|
|
else:
|
|
data.append(chunk)
|
|
return bytesjoin(data)
|
|
|
|
|
|
def findEncryptedChunks(data):
|
|
chunks = []
|
|
while True:
|
|
eBegin = data.find(EEXECBEGIN)
|
|
if eBegin < 0:
|
|
break
|
|
eBegin = eBegin + len(EEXECBEGIN) + 1
|
|
endMatch = EEXECEND.search(data, eBegin)
|
|
if endMatch is None:
|
|
raise T1Error("can't find end of eexec part")
|
|
eEnd = endMatch.start()
|
|
cypherText = data[eBegin : eEnd + 2]
|
|
if isHex(cypherText[:4]):
|
|
cypherText = deHexString(cypherText)
|
|
plainText, R = eexec.decrypt(cypherText, 55665)
|
|
eEndLocal = plainText.find(EEXECINTERNALEND)
|
|
if eEndLocal < 0:
|
|
raise T1Error("can't find end of eexec part")
|
|
chunks.append((0, data[:eBegin]))
|
|
chunks.append((1, cypherText[: eEndLocal + len(EEXECINTERNALEND) + 1]))
|
|
data = data[eEnd:]
|
|
chunks.append((0, data))
|
|
return chunks
|
|
|
|
|
|
def deHexString(hexstring):
|
|
return eexec.deHexString(bytesjoin(hexstring.split()))
|
|
|
|
|
|
# Type 1 assertion
|
|
|
|
_fontType1RE = re.compile(rb"/FontType\s+1\s+def")
|
|
|
|
|
|
def assertType1(data):
|
|
for head in [b"%!PS-AdobeFont", b"%!FontType1"]:
|
|
if data[: len(head)] == head:
|
|
break
|
|
else:
|
|
raise T1Error("not a PostScript font")
|
|
if not _fontType1RE.search(data):
|
|
raise T1Error("not a Type 1 font")
|
|
if data.find(b"currentfile eexec") < 0:
|
|
raise T1Error("not an encrypted Type 1 font")
|
|
# XXX what else?
|
|
return data
|
|
|
|
|
|
# pfb helpers
|
|
|
|
|
|
def longToString(long):
|
|
s = b""
|
|
for i in range(4):
|
|
s += bytechr((long & (0xFF << (i * 8))) >> i * 8)
|
|
return s
|
|
|
|
|
|
def stringToLong(s):
|
|
if len(s) != 4:
|
|
raise ValueError("string must be 4 bytes long")
|
|
l = 0
|
|
for i in range(4):
|
|
l += byteord(s[i]) << (i * 8)
|
|
return l
|
|
|
|
|
|
# PS stream helpers
|
|
|
|
font_dictionary_keys = list(_type1_pre_eexec_order)
|
|
# t1write.c:writeRegNameKeyedFont
|
|
# always counts following keys
|
|
font_dictionary_keys.remove("FontMatrix")
|
|
|
|
FontInfo_dictionary_keys = list(_type1_fontinfo_order)
|
|
# extend because AFDKO tx may use following keys
|
|
FontInfo_dictionary_keys.extend(
|
|
[
|
|
"FSType",
|
|
"Copyright",
|
|
]
|
|
)
|
|
|
|
Private_dictionary_keys = [
|
|
# We don't know what names will be actually used.
|
|
# "RD",
|
|
# "ND",
|
|
# "NP",
|
|
"Subrs",
|
|
"OtherSubrs",
|
|
"UniqueID",
|
|
"BlueValues",
|
|
"OtherBlues",
|
|
"FamilyBlues",
|
|
"FamilyOtherBlues",
|
|
"BlueScale",
|
|
"BlueShift",
|
|
"BlueFuzz",
|
|
"StdHW",
|
|
"StdVW",
|
|
"StemSnapH",
|
|
"StemSnapV",
|
|
"ForceBold",
|
|
"LanguageGroup",
|
|
"password",
|
|
"lenIV",
|
|
"MinFeature",
|
|
"RndStemUp",
|
|
]
|
|
|
|
# t1write_hintothers.h
|
|
hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869
|
|
systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup
|
|
/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def"""
|
|
# t1write.c:saveStdSubrs
|
|
std_subrs = [
|
|
# 3 0 callother pop pop setcurrentpoint return
|
|
b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b",
|
|
# 0 1 callother return
|
|
b"\x8b\x8c\x0c\x10\x0b",
|
|
# 0 2 callother return
|
|
b"\x8b\x8d\x0c\x10\x0b",
|
|
# return
|
|
b"\x0b",
|
|
# 3 1 3 callother pop callsubr return
|
|
b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b",
|
|
]
|
|
# follow t1write.c:writeRegNameKeyedFont
|
|
eexec_IV = b"cccc"
|
|
char_IV = b"\x0c\x0c\x0c\x0c"
|
|
RD_value = ("string", "currentfile", "exch", "readstring", "pop")
|
|
ND_values = [("def",), ("noaccess", "def")]
|
|
PD_values = [("put",), ("noaccess", "put")]
|